├── .env.example
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── README.zh-CN.md
├── components.json
├── docs
├── api-reference.md
├── architecture.md
├── component-guide.md
├── deployment-guide.md
├── i18n-guide.md
└── quick-start.md
├── env-template.txt
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── preview.png
├── public
├── android-chrome-192x192.png
├── apple-icon.png
├── favicon-16x16.png
├── favicon-32x32-original.png
├── favicon-32x32.png
├── favicon.ico
├── images
│ ├── 07b1a04b-cc56-481f-a8cd-8e1618989898_0.png
│ ├── 5a8ccbe9-3bee-4eff-aff1-ff0e7555881b_0.png
│ ├── 5b45db9a-e2d7-49bd-a0aa-d1d96658f0ad_0.png
│ ├── generator-background.jpg
│ └── my-background.jpg
├── imgs
│ ├── cnpay.png
│ ├── icons
│ │ ├── 1.svg
│ │ ├── 2.svg
│ │ ├── 3.svg
│ │ ├── 4.svg
│ │ ├── 5.svg
│ │ └── 6.svg
│ ├── logos
│ │ ├── nextjs.svg
│ │ ├── react.svg
│ │ ├── shadcn.svg
│ │ ├── supabase.svg
│ │ ├── tailwindcss.svg
│ │ └── vercel.svg
│ ├── masks
│ │ ├── circle.svg
│ │ └── line.svg
│ └── placeholder.png
├── logo.png
├── robots.txt.example
├── shortcut-icon.png
├── site.webmanifest
└── sitemap.xml.example
├── src
├── app
│ ├── (legal)
│ │ ├── error.tsx
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── privacy-policy
│ │ │ └── page.mdx
│ │ └── terms-of-service
│ │ │ └── page.mdx
│ ├── [locale]
│ │ ├── (admin)
│ │ │ ├── admin
│ │ │ │ ├── error.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ ├── paid-orders
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── posts
│ │ │ │ │ ├── [uuid]
│ │ │ │ │ │ └── edit
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── add
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ └── users
│ │ │ │ │ └── page.tsx
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ └── loading.tsx
│ │ ├── (default)
│ │ │ ├── (console)
│ │ │ │ ├── api-keys
│ │ │ │ │ ├── create
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── error.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── my-credits
│ │ │ │ │ └── page.tsx
│ │ │ │ └── my-orders
│ │ │ │ │ └── page.tsx
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── posts
│ │ │ │ ├── [slug]
│ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ ├── auth
│ │ │ ├── error.tsx
│ │ │ ├── loading.tsx
│ │ │ └── signin
│ │ │ │ ├── error.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ ├── blog
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ └── loading.tsx
│ │ ├── error.tsx
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── not-found.tsx
│ │ └── pay-success
│ │ │ ├── [session_id]
│ │ │ └── page.tsx
│ │ │ ├── error.tsx
│ │ │ └── loading.tsx
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ ├── checkout
│ │ │ └── route.ts
│ │ ├── error.ts
│ │ ├── image
│ │ │ └── route.ts
│ │ ├── ping
│ │ │ └── route.ts
│ │ └── stripe-notify
│ │ │ └── route.ts
│ ├── error.tsx
│ ├── global-error.tsx
│ ├── globals.css
│ ├── layout.tsx
│ ├── loading.tsx
│ ├── not-found.global.tsx
│ ├── page.tsx
│ ├── sitemap.ts
│ ├── template.tsx
│ └── theme.css
├── auth
│ ├── config.ts
│ ├── index.ts
│ └── session.tsx
├── components
│ ├── blocks
│ │ ├── blog-detail
│ │ │ ├── crumb.tsx
│ │ │ └── index.tsx
│ │ ├── blog
│ │ │ └── index.tsx
│ │ ├── crumb
│ │ │ └── index.tsx
│ │ ├── empty
│ │ │ └── index.tsx
│ │ ├── footer
│ │ │ └── index.tsx
│ │ ├── form
│ │ │ └── index.tsx
│ │ ├── header
│ │ │ └── index.tsx
│ │ ├── hero
│ │ │ ├── announcement-bar.tsx
│ │ │ ├── bg.tsx
│ │ │ ├── floating-image.tsx
│ │ │ ├── happy-users.tsx
│ │ │ ├── index.tsx
│ │ │ └── particles-background.tsx
│ │ ├── pricing
│ │ │ └── index.tsx
│ │ ├── table
│ │ │ ├── copy.tsx
│ │ │ ├── dropdown.tsx
│ │ │ └── index.tsx
│ │ └── toolbar
│ │ │ └── index.tsx
│ ├── console
│ │ ├── layout.tsx
│ │ ├── sidebar
│ │ │ └── nav.tsx
│ │ └── slots
│ │ │ ├── form
│ │ │ └── index.tsx
│ │ │ └── table
│ │ │ └── index.tsx
│ ├── dashboard
│ │ ├── header
│ │ │ └── index.tsx
│ │ ├── layout.tsx
│ │ ├── sidebar
│ │ │ ├── footer.tsx
│ │ │ ├── header.tsx
│ │ │ ├── index.tsx
│ │ │ ├── nav.tsx
│ │ │ └── user.tsx
│ │ └── slots
│ │ │ ├── form
│ │ │ └── index.tsx
│ │ │ └── table
│ │ │ └── index.tsx
│ ├── gallery
│ │ └── gallery.tsx
│ ├── icon
│ │ └── index.tsx
│ ├── image
│ │ ├── Gallery.tsx
│ │ ├── ImageGenerator.tsx
│ │ └── Showcase.tsx
│ ├── layout
│ │ └── app-shell.tsx
│ ├── locale
│ │ └── toggle.tsx
│ ├── markdown
│ │ ├── index.tsx
│ │ └── markdown.css
│ ├── showcase
│ │ └── index.tsx
│ ├── sign
│ │ ├── form.tsx
│ │ ├── modal.tsx
│ │ ├── sign_in.tsx
│ │ ├── toggle.tsx
│ │ └── user.tsx
│ ├── theme-toggle.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── collapsible.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── icon.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── motion.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── radio-group.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── shared-background.tsx
│ │ ├── sheet.tsx
│ │ ├── sidebar.tsx
│ │ ├── skeleton.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ └── tooltip.tsx
├── contexts
│ ├── app.tsx
│ └── theme.tsx
├── data
│ └── install.sql
├── hooks
│ ├── use-mobile.tsx
│ ├── useMediaQuery.tsx
│ └── useOneTapLogin.tsx
├── i18n.ts
├── i18n
│ ├── locale.ts
│ ├── messages
│ │ ├── en.json
│ │ └── zh.json
│ ├── pages
│ │ └── landing
│ │ │ ├── en.json
│ │ │ └── zh.json
│ ├── request.ts
│ ├── routing.ts
│ └── utils.ts
├── lib
│ ├── browser.ts
│ ├── cache.ts
│ ├── hash.ts
│ ├── ip.ts
│ ├── resp.ts
│ ├── storage.ts
│ ├── time.ts
│ ├── tools.ts
│ └── utils.ts
├── mdx-components.tsx
├── middleware.ts
├── models
│ ├── apikey.ts
│ ├── credit.ts
│ ├── db.ts
│ ├── order.ts
│ ├── post.ts
│ └── user.ts
├── providers
│ ├── image
│ │ ├── index.ts
│ │ └── v1
│ │ │ ├── implementations
│ │ │ └── flux.ts
│ │ │ ├── index.ts
│ │ │ ├── interface.ts
│ │ │ └── types.ts
│ └── index.ts
├── services
│ ├── apikey.ts
│ ├── constant.ts
│ ├── credit.ts
│ ├── order.ts
│ ├── page.ts
│ └── user.ts
└── types
│ ├── apikey.d.ts
│ ├── blocks
│ ├── base.d.ts
│ ├── blog.d.ts
│ ├── footer.d.ts
│ ├── form.d.ts
│ ├── header.d.ts
│ ├── hero.d.ts
│ ├── pricing.d.ts
│ ├── section.d.ts
│ ├── sidebar.d.ts
│ └── table.d.ts
│ ├── context.d.ts
│ ├── credit.d.ts
│ ├── global.d.ts
│ ├── mdx.d.ts
│ ├── next-auth.d.ts
│ ├── order.d.ts
│ ├── pages
│ └── landing.d.ts
│ ├── post.d.ts
│ ├── slots
│ ├── base.d.ts
│ ├── form.d.ts
│ └── table.d.ts
│ └── user.d.ts
├── tailwind.config.ts
├── tsconfig.json
└── vercel.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug报告
3 | about: 创建Bug报告以帮助我们改进
4 | title: '[BUG] '
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | ## Bug描述
10 | 请清晰简洁地描述这个Bug
11 |
12 | ## 复现步骤
13 | 复现此行为的步骤:
14 | 1. 前往 '...'
15 | 2. 点击 '....'
16 | 3. 滚动至 '....'
17 | 4. 看到错误
18 |
19 | ## 预期行为
20 | 清晰简洁地描述您期望发生的情况
21 |
22 | ## 截图
23 | 如果适用,添加截图以帮助说明您的问题
24 |
25 | ## 环境信息
26 | - 操作系统: [例如 Windows, macOS, Linux]
27 | - 浏览器 [例如 Chrome, Safari, Firefox]
28 | - 版本 [例如 22]
29 | - Node.js版本: [例如 18.12.0]
30 | - npm版本: [例如 8.19.2]
31 |
32 | ## 额外上下文
33 | 在此处添加有关问题的任何其他上下文
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 功能请求
3 | about: 为这个项目提出一个想法
4 | title: '[功能] '
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 | ## 当前问题
10 | 您的功能请求是否与问题相关?请清晰简洁地描述问题所在。例如,当 [...] 时我总是感到沮丧
11 |
12 | ## 解决方案
13 | 清晰简洁地描述您希望发生的事情
14 |
15 | ## 替代方案
16 | 清晰简洁地描述您考虑过的任何替代解决方案或功能
17 |
18 | ## 实现思路
19 | 如果有关于如何实现的具体想法,请在此处描述
20 |
21 | ## 额外上下文
22 | 在此处添加有关功能请求的任何其他上下文或截图
23 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # 描述
2 |
3 | 请描述这个PR的目的和解决的问题。如果它与某个issue相关,请引用该issue。
4 |
5 | ## 变更类型
6 |
7 | 请在相关选项前标记 [x]:
8 |
9 | - [ ] 功能新增
10 | - [ ] Bug修复
11 | - [ ] 代码风格更新 (格式化, 变量命名)
12 | - [ ] 重构 (无功能变化)
13 | - [ ] 构建相关变更
14 | - [ ] 文档更新
15 | - [ ] 其他(请描述):
16 |
17 | ## 测试情况
18 |
19 | 请描述您进行了哪些测试。
20 | 提供清晰的测试说明,以便审核者可以验证您的变更。
21 |
22 | ## 截图 (如适用)
23 |
24 | ## 检查清单:
25 |
26 | - [ ] 我的代码遵循本项目的代码风格
27 | - [ ] 我已经自我审查了自己的代码
28 | - [ ] 我已经为我的代码写了文档
29 | - [ ] 我已经为我的代码添加了测试
30 | - [ ] 新的和现有的单元测试在本地都通过了
31 | - [ ] 任何依赖项更改都已记录在CHANGELOG.md中
32 | - [ ] 我已更新了必要的文档 (如适用)
33 |
34 | ## 附加信息
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env
30 | .env.local
31 | .env.development
32 | .env.production
33 |
34 | # vercel
35 | .vercel
36 | .tmp
37 |
38 | # typescript
39 | *.tsbuildinfo
40 | next-env.d.ts
41 |
42 | Makefile
43 | .wrangler
44 | wrangler.toml
45 | supabase
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "[javascript]": {
4 | "editor.defaultFormatter": "esbenp.prettier-vscode"
5 | },
6 | "i18n-ally.localesPaths": ["i18n/messages"],
7 | "i18n-ally.keystyle": "nested"
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 scottcwy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚡ Flux Generator
2 |
3 |
4 |
5 | [简体中文](./README.zh-CN.md) | English
6 |
7 |
8 |
9 | ✨ A clean and modern AI image generator built with Next.js, Tailwind CSS, and shadcn/ui. Generate stunning images with simple prompts.
10 |
11 | ## 🌟 Preview
12 |
13 |
14 |
15 |
16 |
17 |
🎨 Clean and Modern Interface
18 |
19 |
20 |
21 | ## 🚀 Quick Start
22 |
23 | ```bash
24 | # Clone the repository
25 | git clone https://github.com/scottcwy/AI-Wallpaper.git
26 |
27 | # Install dependencies
28 | pnpm install
29 |
30 | # Start development server
31 | pnpm dev
32 | ```
33 |
34 | ## ✨ Features
35 |
36 | - **🎨 AI Image Generation**: Create stunning images with simple text prompts
37 | - **⚡ Modern UI**: Built with Tailwind CSS and shadcn/ui components
38 | - **🌐 Internationalization**: Full i18n support with seamless language switching
39 | - **🚀 One-Click Deploy**: Deploy to Vercel with zero configuration
40 | - **📱 Responsive Design**: Perfect on desktop, tablet, and mobile devices
41 | - **🔐 Authentication**: Secure user system with multiple providers
42 |
43 | ## ⚙️ Configuration
44 |
45 | ```bash
46 | # Copy environment template
47 | cp .env.example .env.local
48 |
49 | # Required: Authentication secret for session JWT
50 | # Generate a strong secret, for example:
51 | # openssl rand -base64 32
52 | AUTH_SECRET="your_auth_secret_key"
53 |
54 | # Optional: OAuth providers
55 | # Enable Google or GitHub by providing client credentials and toggles
56 | AUTH_GOOGLE_ID="your_google_client_id"
57 | AUTH_GOOGLE_SECRET="your_google_client_secret"
58 | NEXT_PUBLIC_AUTH_GOOGLE_ENABLED="false"
59 |
60 | AUTH_GITHUB_ID="your_github_client_id"
61 | AUTH_GITHUB_SECRET="your_github_client_secret"
62 | NEXT_PUBLIC_AUTH_GITHUB_ENABLED="false"
63 |
64 | # Tip: In local development, if you forget to set AUTH_SECRET,
65 | # the app falls back to a dev-only secret to avoid 500 errors.
66 | # In production, always set AUTH_SECRET.
67 | ```
68 |
69 | ## 🎯 Usage
70 |
71 | 1. Enter your image prompt
72 | 2. Click generate and wait for AI magic
73 | 3. Download or share your creation
74 |
75 | That's it! Simple and clean.
76 |
77 | ## 🚀 Deploy
78 |
79 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fscottcwy%2FAI-Wallpaper)
80 |
81 | One-click deployment to Vercel. No configuration needed.
82 |
83 | ## 🛠️ Tech Stack
84 |
85 | - **Next.js 14** - React framework with App Router
86 | - **Tailwind CSS** - Utility-first CSS framework
87 | - **shadcn/ui** - Beautiful and accessible components
88 | - **i18n** - Internationalization support
89 | - **Vercel** - Deployment platform
90 |
91 | ## 📄 License
92 |
93 | MIT License - see [LICENSE](LICENSE) for details.
94 |
95 | ---
96 |
97 | **Simple. Clean. Powerful. 🎨**
98 |
99 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # 🎨 AI 壁纸生成器
2 |
3 |
4 |
5 | 简体中文 | [English](./README.md)
6 |
7 |
8 |
9 | ✨ 基于 Next.js 构建的 AI 壁纸生成器。使用tailwind css,shadcn ui,以及i18n系统,支持vercel一键部署。
10 |
11 | ## 🌟 平台预览
12 |
13 |
14 |
15 |
16 |
17 |
🎨 AI 壁纸生成界面
18 |
19 |
20 |
21 | ## 🛠️ 环境要求
22 |
23 | - Node.js 20.x 或更高版本
24 | - Next.js 14.2.9
25 | - pnpm (推荐) 或 npm
26 |
27 | ## 🚀 快速开始
28 |
29 | ```bash
30 | # 克隆仓库
31 | git clone https://github.com/scottcwy/AI-Wallpaper.git
32 |
33 | # 安装依赖
34 | pnpm install
35 |
36 | # 启动开发服务器
37 | pnpm dev
38 | ```
39 |
40 | ## ✨ 核心特性
41 |
42 | - **🎨 AI 图像生成**: 通过简单的文本提示创建精美图像
43 | - **⚡ 现代界面**: 使用 Tailwind CSS 和 shadcn/ui 组件构建
44 | - **🌐 国际化**: 完整的 i18n 支持,无缝语言切换
45 | - **🚀 一键部署**: 零配置部署到 Vercel
46 | - **📱 响应式设计**: 在桌面、平板和移动设备上完美显示
47 | - **🔐 用户认证**: 支持多种提供商的安全用户系统
48 |
49 | ## ⚙️ 配置
50 |
51 | ```bash
52 | # 复制环境变量模板
53 | cp .env.example .env.local
54 |
55 | # 配置 AI 服务
56 | OPENAI_API_KEY="your_openai_api_key"
57 |
58 | # 可选:身份验证
59 | GOOGLE_CLIENT_ID="your_google_client_id"
60 | GOOGLE_CLIENT_SECRET="your_google_client_secret"
61 | ```
62 |
63 | ## 🎯 使用方法
64 |
65 | 1. 输入您的图像提示词
66 | 2. 点击生成并等待 AI 魔法
67 | 3. 下载或分享您的创作
68 |
69 | 就是这么简单!简洁而强大。
70 |
71 | ## 🚀 部署
72 |
73 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fscottcwy%2FAI-Wallpaper)
74 |
75 | 一键部署到 Vercel,无需配置。
76 |
77 | ## 🛠️ 技术栈
78 |
79 | - **Next.js 14** - 带有 App Router 的 React 框架
80 | - **Tailwind CSS** - 实用优先的 CSS 框架
81 | - **shadcn/ui** - 美观且易用的组件库
82 | - **i18n** - 国际化支持
83 | - **Vercel** - 部署平台
84 |
85 | ## 📄 许可证
86 |
87 | MIT 许可证 - 详情请参阅 [LICENSE](LICENSE)。
88 |
89 | ---
90 |
91 | **简洁。干净。强大。🎨**
92 |
93 |
94 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/env-template.txt:
--------------------------------------------------------------------------------
1 | # 认证相关配置(Auth.js v5)
2 | # 必填:用于加密会话和JWT,请使用至少32位的强随机字符串
3 | AUTH_SECRET=your-auth-secret-key
4 |
5 | # 可选:仅当你的应用部署在反向代理后或需要显式信任主机时
6 | # AUTH_TRUST_HOST=true
7 | # 可选:如果使用自定义基础路径或特殊部署场景,可设置 AUTH_URL
8 | # AUTH_URL=http://localhost:3000
9 |
10 | # Coze API配置
11 | COZE_API_URL="https://api.coze.cn"
12 | COZE_API_KEY="pat_7UmKEVl4rtjCqJiAnlAy7jmP14vqRmROLsanFxs4ZhHyLJh2XRBD1ooW1ct3MRgm"
13 | COZE_BOT_ID="7488591335084146697"
14 |
15 | # 认证提供商配置
16 | NEXT_PUBLIC_AUTH_GOOGLE_ENABLED=false
17 | NEXT_PUBLIC_AUTH_GITHUB_ENABLED=false
18 | NEXT_PUBLIC_AUTH_GOOGLE_ONE_TAP_ENABLED=false
19 |
20 | # 网站URL配置
21 | NEXT_PUBLIC_WEB_URL=http://localhost:3000
22 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import bundleAnalyzer from "@next/bundle-analyzer";
2 | import createNextIntlPlugin from "next-intl/plugin";
3 | import mdx from "@next/mdx";
4 |
5 | const withBundleAnalyzer = bundleAnalyzer({
6 | enabled: process.env.ANALYZE === "true",
7 | });
8 |
9 | const withNextIntl = createNextIntlPlugin();
10 |
11 | const withMDX = mdx({
12 | options: {
13 | remarkPlugins: [],
14 | rehypePlugins: [],
15 | },
16 | });
17 |
18 | /** @type {import('next').NextConfig} */
19 | const nextConfig = {
20 | output: "standalone",
21 | reactStrictMode: true,
22 | pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
23 | images: {
24 | domains: ['localhost', 'cdn.example.com'],
25 | remotePatterns: [
26 | {
27 | protocol: "https",
28 | hostname: "*",
29 | },
30 | ],
31 | },
32 | experimental: {
33 | mdxRs: true,
34 | serverComponentsExternalPackages: ['sharp', 'canvas'],
35 | },
36 | async redirects() {
37 | return [];
38 | },
39 | };
40 |
41 | export default withBundleAnalyzer(withNextIntl(withMDX(nextConfig)));
42 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/preview.png
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/apple-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/favicon-32x32-original.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/07b1a04b-cc56-481f-a8cd-8e1618989898_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/images/07b1a04b-cc56-481f-a8cd-8e1618989898_0.png
--------------------------------------------------------------------------------
/public/images/5a8ccbe9-3bee-4eff-aff1-ff0e7555881b_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/images/5a8ccbe9-3bee-4eff-aff1-ff0e7555881b_0.png
--------------------------------------------------------------------------------
/public/images/5b45db9a-e2d7-49bd-a0aa-d1d96658f0ad_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/images/5b45db9a-e2d7-49bd-a0aa-d1d96658f0ad_0.png
--------------------------------------------------------------------------------
/public/images/generator-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/images/generator-background.jpg
--------------------------------------------------------------------------------
/public/images/my-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/images/my-background.jpg
--------------------------------------------------------------------------------
/public/imgs/cnpay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/imgs/cnpay.png
--------------------------------------------------------------------------------
/public/imgs/icons/1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/imgs/icons/2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/imgs/icons/3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/public/imgs/icons/4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/imgs/icons/5.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/public/imgs/icons/6.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/imgs/logos/nextjs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/public/imgs/logos/vercel.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/imgs/masks/circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/imgs/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/imgs/placeholder.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/logo.png
--------------------------------------------------------------------------------
/public/robots.txt.example:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 | Disallow: /*?*q=
4 | Disallow: /privacy-policy
5 | Disallow: /terms-of-service
6 | Disallow: /admin/
7 | Disallow: /(admin)/
8 |
9 | # 站点地图
10 | Sitemap: https://www.example.com/sitemap.xml
11 |
12 | # 主机
13 | Host: https://www.example.com
14 |
--------------------------------------------------------------------------------
/public/shortcut-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scottcwy/Flux-Generator/20aba01aaaa29f88178a8b4a2feb84f35194b132/public/shortcut-icon.png
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"background_color":"#ffffff","display":"standalone","icons":[{"sizes":"192x192","src":"/android-chrome-192x192.png","type":"image/png"},{"sizes":"512x512","src":"/android-chrome-512x512.png","type":"image/png"}],"name":"Katana-NextJS","short_name":"Katana","theme_color":"#ffffff"}
--------------------------------------------------------------------------------
/public/sitemap.xml.example:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://www.example.com
5 | 2022-02-28T07:28:24+00:00
6 | daily
7 | 1.0
8 |
9 |
10 | https://www.example.com/en
11 | 2022-02-28T07:28:24+00:00
12 | daily
13 | 1.0
14 |
15 |
16 | https://www.example.com/zh
17 | 2022-02-28T07:28:24+00:00
18 | daily
19 | 1.0
20 |
21 |
22 | https://www.example.com/blog
23 | 2022-02-28T07:28:24+00:00
24 | weekly
25 | 0.8
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/app/(legal)/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 |
6 | export default function LegalLayoutError({
7 | error,
8 | reset,
9 | }: {
10 | error: Error & { digest?: string };
11 | reset: () => void;
12 | }) {
13 | useEffect(() => {
14 | // u8bb0u5f55u9519u8befu5230u9519u8befu62a5u544au670du52a1
15 | console.error(error);
16 | }, [error]);
17 |
18 | return (
19 |
20 |
u51fau73b0u4e86u4e00u4e9bu95eeu9898
21 |
22 | u5f88u62b1u6b49uff0cu6211u4eecu9047u5230u4e86u4e00u4e2au9519u8befu3002u8bf7u5c1du8bd5u5237u65b0u9875u9762u6216u8fd4u56deu9996u9875u3002
23 |
24 |
25 | reset()} variant="default">
26 | u91cdu8bd5
27 |
28 | window.location.href = '/'} variant="outline">
29 | u8fd4u56deu9996u9875
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/(legal)/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/app/globals.css";
2 |
3 | import { MdOutlineHome } from "react-icons/md";
4 | import { Metadata } from "next";
5 | import React from "react";
6 | import { getTranslations } from "next-intl/server";
7 |
8 | export async function generateMetadata(): Promise {
9 | const t = await getTranslations();
10 |
11 | return {
12 | title: {
13 | template: `%s | ${t("metadata.title")}`,
14 | default: t("metadata.title"),
15 | },
16 | description: t("metadata.description"),
17 | keywords: t("metadata.keywords"),
18 | };
19 | }
20 |
21 | export default function LegalLayout({
22 | children,
23 | }: {
24 | children: React.ReactNode;
25 | }) {
26 | return (
27 |
28 |
29 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/(legal)/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function LegalLayoutLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/admin/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function AdminPageError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | // 记录错误到错误报告服务
18 | console.error(error);
19 | }, [error]);
20 |
21 | return (
22 |
23 |
{t('title')}
24 |
25 | {t('message')}
26 |
27 |
28 | reset()} variant="default">
29 | {t('retry')}
30 |
31 | window.location.href = '/'} variant="outline">
32 | {t('home')}
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/admin/page.tsx:
--------------------------------------------------------------------------------
1 | import Empty from "@/components/blocks/empty";
2 |
3 | export default function () {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/admin/paid-orders/page.tsx:
--------------------------------------------------------------------------------
1 | import { TableColumn } from "@/types/blocks/table";
2 | import TableSlot from "@/components/dashboard/slots/table";
3 | import { Table as TableSlotType } from "@/types/slots/table";
4 | import { getPaiedOrders } from "@/models/order";
5 | import moment from "moment";
6 |
7 | export default async function () {
8 | const orders = await getPaiedOrders(1, 50);
9 |
10 | const columns: TableColumn[] = [
11 | { name: "order_no", title: "Order No" },
12 | { name: "paid_email", title: "Paid Email" },
13 | { name: "product_name", title: "Product Name" },
14 | { name: "amount", title: "Amount" },
15 | {
16 | name: "created_at",
17 | title: "Created At",
18 | callback: (row) => moment(row.created_at).format("YYYY-MM-DD HH:mm:ss"),
19 | },
20 | ];
21 |
22 | const table: TableSlotType = {
23 | title: "Paid Orders",
24 | columns,
25 | data: orders,
26 | };
27 |
28 | return ;
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/admin/posts/page.tsx:
--------------------------------------------------------------------------------
1 | import Dropdown from "@/components/blocks/table/dropdown";
2 | import { NavItem } from "@/types/blocks/base";
3 | import { Post } from "@/types/post";
4 | import TableSlot from "@/components/dashboard/slots/table";
5 | import { Table as TableSlotType } from "@/types/slots/table";
6 | import { getAllPosts } from "@/models/post";
7 | import moment from "moment";
8 |
9 | export default async function () {
10 | const posts = await getAllPosts();
11 |
12 | const table: TableSlotType = {
13 | title: "Posts",
14 | toolbar: {
15 | items: [
16 | {
17 | title: "Add Post",
18 | icon: "RiAddLine",
19 | url: "/admin/posts/add",
20 | },
21 | ],
22 | },
23 | columns: [
24 | {
25 | name: "title",
26 | title: "Title",
27 | },
28 | {
29 | name: "description",
30 | title: "Description",
31 | },
32 | {
33 | name: "slug",
34 | title: "Slug",
35 | },
36 | {
37 | name: "locale",
38 | title: "Locale",
39 | },
40 | {
41 | name: "status",
42 | title: "Status",
43 | },
44 | {
45 | name: "created_at",
46 | title: "Created At",
47 | callback: (item: Post) => {
48 | return moment(item.created_at).format("YYYY-MM-DD HH:mm:ss");
49 | },
50 | },
51 | {
52 | callback: (item: Post) => {
53 | const items: NavItem[] = [
54 | {
55 | title: "Edit",
56 | icon: "RiEditLine",
57 | url: `/admin/posts/${item.uuid}/edit`,
58 | },
59 | {
60 | title: "View",
61 | icon: "RiEyeLine",
62 | url: `/${item.locale}/posts/${item.slug}`,
63 | target: "_blank",
64 | },
65 | ];
66 |
67 | return ;
68 | },
69 | },
70 | ],
71 | data: posts,
72 | empty_message: "No posts found",
73 | };
74 |
75 | return ;
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/admin/users/page.tsx:
--------------------------------------------------------------------------------
1 | import { TableColumn } from "@/types/blocks/table";
2 | import TableSlot from "@/components/dashboard/slots/table";
3 | import { Table as TableSlotType } from "@/types/slots/table";
4 | import { getUsers } from "@/models/user";
5 | import moment from "moment";
6 |
7 | export default async function () {
8 | const users = await getUsers(1, 50);
9 |
10 | const columns: TableColumn[] = [
11 | { name: "uuid", title: "UUID" },
12 | { name: "email", title: "Email" },
13 | { name: "nickname", title: "Name" },
14 | {
15 | name: "avatar_url",
16 | title: "Avatar",
17 | callback: (row) => (
18 |
19 | ),
20 | },
21 | {
22 | name: "created_at",
23 | title: "Created At",
24 | callback: (row) => moment(row.created_at).format("YYYY-MM-DD HH:mm:ss"),
25 | },
26 | ];
27 |
28 | const table: TableSlotType = {
29 | title: "All Users",
30 | columns,
31 | data: users,
32 | };
33 |
34 | return ;
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function AdminLayoutError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | // u8bb0u5f55u9519u8befu5230u9519u8befu62a5u544au670du52a1
18 | console.error(error);
19 | }, [error]);
20 |
21 | return (
22 |
23 |
{t('title')}
24 |
25 | {t('message')}
26 |
27 |
28 | reset()} variant="default">
29 | {t('retry')}
30 |
31 | {
33 | // 从URL路径中获取当前locale
34 | const pathParts = window.location.pathname.split('/');
35 | const locale = pathParts[1] || 'en'; // 默认英语
36 | window.location.href = `/${locale}`;
37 | }}
38 | variant="outline"
39 | >
40 | {t('home')}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/layout.tsx:
--------------------------------------------------------------------------------
1 | import DashboardLayout from "@/components/dashboard/layout";
2 | import Empty from "@/components/blocks/empty";
3 | import { ReactNode } from "react";
4 | import { Sidebar } from "@/types/blocks/sidebar";
5 | import { getUserInfo } from "@/services/user";
6 | import { redirect } from "next/navigation";
7 |
8 | export default async function AdminLayout({
9 | children,
10 | }: {
11 | children: ReactNode;
12 | }) {
13 | const userInfo = await getUserInfo();
14 | if (!userInfo || !userInfo.email) {
15 | redirect("/auth/signin");
16 | }
17 |
18 | const adminEmails = process.env.ADMIN_EMAILS?.split(",");
19 | if (!adminEmails?.includes(userInfo?.email)) {
20 | return ;
21 | }
22 |
23 | const sidebar: Sidebar = {
24 | brand: {
25 | title: "Katana-NextJS",
26 | logo: {
27 | src: "/logo.png",
28 | alt: "Katana-NextJS",
29 | },
30 | url: "",
31 | },
32 | nav: {
33 | items: [
34 | {
35 | title: "Users",
36 | url: "/admin/users",
37 | icon: "RiUserLine",
38 | },
39 | {
40 | title: "Orders",
41 | icon: "RiOrderPlayLine",
42 | is_expand: true,
43 | children: [
44 | {
45 | title: "Paid Orders",
46 | url: "/admin/paid-orders",
47 | },
48 | ],
49 | },
50 | {
51 | title: "Posts",
52 | url: "/admin/posts",
53 | icon: "RiArticleLine",
54 | },
55 | ],
56 | },
57 | social: {
58 | items: [
59 | {
60 | title: "Home",
61 | url: "",
62 | target: "_blank",
63 | icon: "RiHomeLine",
64 | },
65 | {
66 | title: "Github",
67 | url: "",
68 | target: "_blank",
69 | icon: "RiGithubLine",
70 | },
71 | {
72 | title: "Discord",
73 | url: "https://discord.gg/HQNnrzjZQS",
74 | target: "_blank",
75 | icon: "RiDiscordLine",
76 | },
77 | {
78 | title: "X",
79 | url: "",
80 | target: "_blank",
81 | icon: "RiTwitterLine",
82 | },
83 | ],
84 | },
85 | };
86 |
87 | return {children} ;
88 | }
89 |
--------------------------------------------------------------------------------
/src/app/[locale]/(admin)/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function AdminLayoutLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/(console)/api-keys/create/page.tsx:
--------------------------------------------------------------------------------
1 | import { ApikeyStatus, insertApikey } from "@/models/apikey";
2 |
3 | import { Apikey } from "@/types/apikey";
4 | import Empty from "@/components/blocks/empty";
5 | import FormSlot from "@/components/console/slots/form";
6 | import { Form as FormSlotType } from "@/types/slots/form";
7 | import { getIsoTimestr } from "@/lib/time";
8 | import { getNonceStr } from "@/lib/hash";
9 | import { getTranslations } from "next-intl/server";
10 | import { getUserUuid } from "@/services/user";
11 |
12 | export default async function () {
13 | const t = await getTranslations();
14 |
15 | const user_uuid = await getUserUuid();
16 | if (!user_uuid) {
17 | return ;
18 | }
19 |
20 | const form: FormSlotType = {
21 | title: t("api_keys.create_api_key"),
22 | crumb: {
23 | items: [
24 | {
25 | title: t("api_keys.title"),
26 | url: "/api-keys",
27 | },
28 | {
29 | title: t("api_keys.create_api_key"),
30 | is_active: true,
31 | },
32 | ],
33 | },
34 | fields: [
35 | {
36 | title: t("api_keys.form.name"),
37 | name: "title",
38 | type: "text",
39 | placeholder: t("api_keys.form.name_placeholder"),
40 | validation: {
41 | required: true,
42 | },
43 | },
44 | ],
45 | passby: {
46 | user_uuid,
47 | },
48 | submit: {
49 | button: {
50 | title: t("api_keys.form.submit"),
51 | },
52 | handler: async (data: FormData, passby: any) => {
53 | "use server";
54 |
55 | const { user_uuid } = passby;
56 | if (!user_uuid) {
57 | throw new Error("no auth");
58 | }
59 |
60 | const title = data.get("title") as string;
61 | if (!title || !title.trim()) {
62 | throw new Error("invalid params");
63 | }
64 |
65 | const key = `sk-${getNonceStr(32)}`;
66 |
67 | const apikey: Apikey = {
68 | user_uuid,
69 | api_key: key,
70 | title,
71 | created_at: getIsoTimestr(),
72 | status: ApikeyStatus.Created,
73 | };
74 |
75 | try {
76 | await insertApikey(apikey);
77 |
78 | return {
79 | status: "success",
80 | message: "apikey created",
81 | redirect_url: "/api-keys",
82 | };
83 | } catch (e: any) {
84 | console.error(e);
85 | throw new Error("create api key failed: " + e.message);
86 | }
87 | },
88 | },
89 | };
90 |
91 | return ;
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/(console)/api-keys/page.tsx:
--------------------------------------------------------------------------------
1 | import Empty from "@/components/blocks/empty";
2 | import TableSlot from "@/components/console/slots/table";
3 | import { Table as TableSlotType } from "@/types/slots/table";
4 | import { getTranslations } from "next-intl/server";
5 | import { getUserApikeys } from "@/models/apikey";
6 | import { getUserUuid } from "@/services/user";
7 | import moment from "moment";
8 |
9 | export default async function () {
10 | const t = await getTranslations();
11 |
12 | const user_uuid = await getUserUuid();
13 | if (!user_uuid) {
14 | return ;
15 | }
16 |
17 | const data = await getUserApikeys(user_uuid);
18 |
19 | const table: TableSlotType = {
20 | title: t("api_keys.title"),
21 | tip: {
22 | title: t("api_keys.tip"),
23 | },
24 | toolbar: {
25 | items: [
26 | {
27 | title: t("api_keys.create_api_key"),
28 | url: "/api-keys/create",
29 | icon: "RiAddLine",
30 | },
31 | ],
32 | },
33 | columns: [
34 | {
35 | title: t("api_keys.table.name"),
36 | name: "title",
37 | },
38 | {
39 | title: t("api_keys.table.key"),
40 | name: "api_key",
41 | type: "copy",
42 | callback: (item: any) => {
43 | return item.api_key.slice(0, 4) + "..." + item.api_key.slice(-4);
44 | },
45 | },
46 | {
47 | title: t("api_keys.table.created_at"),
48 | name: "created_at",
49 | callback: (item: any) => {
50 | return moment(item.created_at).fromNow();
51 | },
52 | },
53 | ],
54 | data,
55 | empty_message: t("api_keys.no_api_keys"),
56 | };
57 |
58 | return ;
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/(console)/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function ConsoleLayoutError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | // u8bb0u5f55u9519u8befu5230u9519u8befu62a5u544au670du52a1
18 | console.error(error);
19 | }, [error]);
20 |
21 | return (
22 |
23 |
{t('title')}
24 |
25 | {t('message')}
26 |
27 |
28 | reset()} variant="default">
29 | {t('retry')}
30 |
31 | {
33 | // 从URL路径中获取当前locale
34 | const pathParts = window.location.pathname.split('/');
35 | const locale = pathParts[1] || 'en'; // 默认英语
36 | window.location.href = `/${locale}`;
37 | }}
38 | variant="outline"
39 | >
40 | {t('home')}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/(console)/layout.tsx:
--------------------------------------------------------------------------------
1 | import ConsoleLayout from "@/components/console/layout";
2 | import { ReactNode } from "react";
3 | import { Sidebar } from "@/types/blocks/sidebar";
4 | import { getTranslations } from "next-intl/server";
5 | import { getUserInfo } from "@/services/user";
6 | import { redirect } from "next/navigation";
7 |
8 | export default async function ({ children }: { children: ReactNode }) {
9 | const userInfo = await getUserInfo();
10 | if (!userInfo || !userInfo.email) {
11 | redirect("/auth/signin");
12 | }
13 |
14 | const t = await getTranslations();
15 |
16 | const sidebar: Sidebar = {
17 | nav: {
18 | items: [
19 | {
20 | title: t("user.my_orders"),
21 | url: "/my-orders",
22 | icon: "RiOrderPlayLine",
23 | is_active: false,
24 | },
25 | {
26 | title: t("my_credits.title"),
27 | url: "/my-credits",
28 | icon: "RiBankCardLine",
29 | is_active: false,
30 | },
31 | {
32 | title: t("api_keys.title"),
33 | url: "/api-keys",
34 | icon: "RiKey2Line",
35 | is_active: false,
36 | },
37 | ],
38 | },
39 | };
40 |
41 | return {children} ;
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/(console)/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function ConsoleLayoutLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/(console)/my-credits/page.tsx:
--------------------------------------------------------------------------------
1 | import Empty from "@/components/blocks/empty";
2 | import TableSlot from "@/components/console/slots/table";
3 | import { Table as TableSlotType } from "@/types/slots/table";
4 | import { getCreditsByUserUuid } from "@/models/credit";
5 | import { getTranslations } from "next-intl/server";
6 | import { getUserCredits } from "@/services/credit";
7 | import { getUserUuid } from "@/services/user";
8 | import moment from "moment";
9 |
10 | export default async function () {
11 | const t = await getTranslations();
12 |
13 | const user_uuid = await getUserUuid();
14 |
15 | if (!user_uuid) {
16 | return ;
17 | }
18 |
19 | const data = await getCreditsByUserUuid(user_uuid, 1, 100);
20 |
21 | const userCredits = await getUserCredits(user_uuid);
22 |
23 | const table: TableSlotType = {
24 | title: t("my_credits.title"),
25 | tip: {
26 | title: t("my_credits.left_tip", {
27 | left_credits: userCredits?.left_credits || 0,
28 | }),
29 | },
30 | toolbar: {
31 | items: [
32 | {
33 | title: t("my_credits.recharge"),
34 | url: "/#pricing",
35 | target: "_blank",
36 | },
37 | ],
38 | },
39 | columns: [
40 | {
41 | title: t("my_credits.table.trans_no"),
42 | name: "trans_no",
43 | },
44 | {
45 | title: t("my_credits.table.trans_type"),
46 | name: "trans_type",
47 | },
48 | {
49 | title: t("my_credits.table.credits"),
50 | name: "credits",
51 | },
52 | {
53 | title: t("my_credits.table.updated_at"),
54 | name: "created_at",
55 | callback: (v: any) => {
56 | return moment(v.created_at).format("YYYY-MM-DD HH:mm:ss");
57 | },
58 | },
59 | ],
60 | data,
61 | empty_message: t("my_credits.no_credits"),
62 | };
63 |
64 | return ;
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/(console)/my-orders/page.tsx:
--------------------------------------------------------------------------------
1 | import { getOrdersByPaidEmail, getOrdersByUserUuid } from "@/models/order";
2 | import { getUserEmail, getUserUuid } from "@/services/user";
3 |
4 | import { TableColumn } from "@/types/blocks/table";
5 | import TableSlot from "@/components/console/slots/table";
6 | import { Table as TableSlotType } from "@/types/slots/table";
7 | import { getTranslations } from "next-intl/server";
8 | import moment from "moment";
9 | import { redirect } from "next/navigation";
10 |
11 | export default async function () {
12 | const t = await getTranslations();
13 |
14 | const user_uuid = await getUserUuid();
15 | const user_email = await getUserEmail();
16 |
17 | const callbackUrl = `${process.env.NEXT_PUBLIC_WEB_URL}/my-orders`;
18 | if (!user_uuid) {
19 | redirect(`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`);
20 | }
21 |
22 | let orders = await getOrdersByUserUuid(user_uuid);
23 | if (!orders || orders.length === 0) {
24 | orders = await getOrdersByPaidEmail(user_email);
25 | }
26 |
27 | const columns: TableColumn[] = [
28 | { name: "order_no", title: t("my_orders.table.order_no") },
29 | { name: "paid_email", title: t("my_orders.table.email") },
30 | { name: "product_name", title: t("my_orders.table.product_name") },
31 | {
32 | name: "amount",
33 | title: t("my_orders.table.amount"),
34 | callback: (item: any) =>
35 | `${item.currency.toUpperCase() === "CNY" ? "¥" : "$"} ${
36 | item.amount / 100
37 | }`,
38 | },
39 | {
40 | name: "paid_at",
41 | title: t("my_orders.table.paid_at"),
42 | callback: (item: any) =>
43 | moment(item.paid_at).format("YYYY-MM-DD HH:mm:ss"),
44 | },
45 | ];
46 |
47 | const table: TableSlotType = {
48 | title: t("my_orders.title"),
49 | description: t("my_orders.description"),
50 | toolbar: {
51 | items: [
52 | {
53 | title: t("my_orders.read_docs"),
54 | icon: "RiBookLine",
55 | url: "",
56 | target: "_blank",
57 | variant: "outline",
58 | },
59 | {
60 | title: t("my_orders.join_discord"),
61 | icon: "RiDiscordFill",
62 | url: "https://discord.gg/HQNnrzjZQS",
63 | target: "_blank",
64 | },
65 | ],
66 | },
67 | columns: columns,
68 | data: orders,
69 | empty_message: t("my_orders.no_orders"),
70 | };
71 |
72 | return ;
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function DefaultLayoutError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | // 记录错误到错误报告服务
18 | console.error(error);
19 | }, [error]);
20 |
21 | return (
22 |
23 |
{t('title')}
24 |
25 | {t('message')}
26 |
27 |
28 | reset()} variant="default">
29 | {t('retry')}
30 |
31 | window.location.href = '/'} variant="outline">
32 | {t('home')}
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "@/components/blocks/footer";
2 | import Header from "@/components/blocks/header";
3 | import { ReactNode } from "react";
4 | import { getLandingPage } from "@/services/page";
5 | import dynamic from 'next/dynamic';
6 | import Image from "next/image";
7 |
8 | const SharedBackground = dynamic(() => import('@/components/ui/shared-background'), {
9 | ssr: false
10 | });
11 |
12 | export default async function DefaultLayout({
13 | children,
14 | params: { locale },
15 | }: {
16 | children: ReactNode;
17 | params: { locale: string };
18 | }) {
19 | const page = await getLandingPage(locale);
20 |
21 | return (
22 | <>
23 | {/* 固定的背景图片 - 应用于整个layout */}
24 |
25 |
32 |
33 |
34 | {/* 注释掉SharedBackground以允许自定义背景图片显示 */}
35 | {/* */}
36 | {page.header && }
37 | {children}
38 | {page.footer && }
39 | >
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function DefaultLayoutLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/page.tsx:
--------------------------------------------------------------------------------
1 | import Hero from "@/components/blocks/hero";
2 | import { getLandingPage } from "@/services/page";
3 | import dynamic from 'next/dynamic';
4 |
5 | const Generator = dynamic(() => import('@/components/image/ImageGenerator').then(mod => mod.ImageGenerator), {
6 | ssr: false
7 | });
8 |
9 | export async function generateMetadata({
10 | params: { locale },
11 | }: {
12 | params: { locale: string };
13 | }) {
14 | let canonicalUrl = `${process.env.NEXT_PUBLIC_WEB_URL}`;
15 |
16 | if (locale !== "en") {
17 | canonicalUrl = `${process.env.NEXT_PUBLIC_WEB_URL}/${locale}`;
18 | }
19 |
20 | return {
21 | alternates: {
22 | canonical: canonicalUrl,
23 | },
24 | };
25 | }
26 |
27 | export default async function LandingPage({
28 | params: { locale },
29 | }: {
30 | params: { locale: string };
31 | }) {
32 | //
33 | // const page = await getLandingPage(locale);
34 |
35 | return (
36 | <>
37 |
38 |
39 | >
40 | );
41 | }
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/posts/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { PostStatus, findPostBySlug } from "@/models/post";
2 |
3 | import BlogDetail from "@/components/blocks/blog-detail";
4 | import Empty from "@/components/blocks/empty";
5 | import { getTranslations } from "next-intl/server";
6 |
7 | export async function generateMetadata({
8 | params,
9 | }: {
10 | params: { locale: string; slug: string };
11 | }) {
12 | const t = await getTranslations();
13 |
14 | const post = await findPostBySlug(params.slug, params.locale);
15 |
16 | let canonicalUrl = `${process.env.NEXT_PUBLIC_WEB_URL}/posts/${params.slug}`;
17 |
18 | if (params.locale !== "en") {
19 | canonicalUrl = `${process.env.NEXT_PUBLIC_WEB_URL}/${params.locale}/posts/${params.slug}`;
20 | }
21 |
22 | return {
23 | title: post?.title,
24 | description: post?.description,
25 | alternates: {
26 | canonical: canonicalUrl,
27 | },
28 | };
29 | }
30 |
31 | export default async function ({
32 | params,
33 | }: {
34 | params: { locale: string; slug: string };
35 | }) {
36 | const post = await findPostBySlug(params.slug, params.locale);
37 |
38 | if (!post || post.status !== PostStatus.Online) {
39 | return ;
40 | }
41 |
42 | return ;
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/[locale]/(default)/posts/page.tsx:
--------------------------------------------------------------------------------
1 | import Blog from "@/components/blocks/blog";
2 | import { Blog as BlogType } from "@/types/blocks/blog";
3 | import { getPostsByLocale } from "@/models/post";
4 | import { getTranslations } from "next-intl/server";
5 |
6 | export async function generateMetadata({
7 | params: { locale },
8 | }: {
9 | params: { locale: string };
10 | }) {
11 | const t = await getTranslations();
12 |
13 | let canonicalUrl = `${process.env.NEXT_PUBLIC_WEB_URL}/posts`;
14 |
15 | if (locale !== "en") {
16 | canonicalUrl = `${process.env.NEXT_PUBLIC_WEB_URL}/${locale}/posts`;
17 | }
18 |
19 | return {
20 | title: t("blog.title"),
21 | description: t("blog.description"),
22 | alternates: {
23 | canonical: canonicalUrl,
24 | },
25 | };
26 | }
27 |
28 | export default async function ({ params }: { params: { locale: string } }) {
29 | const t = await getTranslations();
30 |
31 | const posts = await getPostsByLocale(params.locale);
32 |
33 | const blog: BlogType = {
34 | title: t("blog.title"),
35 | description: t("blog.description"),
36 | items: posts,
37 | read_more_text: t("blog.read_more_text"),
38 | };
39 |
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/[locale]/auth/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function AuthError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | console.error(error);
18 | }, [error]);
19 |
20 | return (
21 |
22 |
{t('title')}
23 |
24 | {t('message')}
25 |
26 |
27 | reset()} variant="default">
28 | {t('retry')}
29 |
30 | {
32 | // 从URL路径中获取当前locale
33 | const pathParts = window.location.pathname.split('/');
34 | const locale = pathParts[1] || 'en'; // 默认英语
35 | window.location.href = `/${locale}`;
36 | }}
37 | variant="outline"
38 | >
39 | {t('home')}
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/[locale]/auth/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function AuthLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/[locale]/auth/signin/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function SignInError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | console.error(error);
18 | }, [error]);
19 |
20 | return (
21 |
22 |
{t('title')}
23 |
24 | {t('message')}
25 |
26 |
27 | reset()} variant="default">
28 | {t('retry')}
29 |
30 | window.location.href = '/'} variant="outline">
31 | {t('home')}
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/[locale]/auth/signin/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function SignInLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/[locale]/auth/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import SignForm from "@/components/sign/form";
2 | import { auth } from "@/auth";
3 | import { redirect } from "next/navigation";
4 |
5 | export default async function SignInPage({
6 | searchParams,
7 | }: {
8 | searchParams: { callbackUrl: string | undefined };
9 | }) {
10 | const session = await auth();
11 | if (session) {
12 | return redirect(searchParams.callbackUrl || "/");
13 | }
14 |
15 | return (
16 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/[locale]/blog/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function BlogError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | console.error(error);
18 | }, [error]);
19 |
20 | return (
21 |
22 |
{t('title')}
23 |
24 | {t('message')}
25 |
26 |
27 | reset()} variant="default">
28 | {t('retry')}
29 |
30 | window.location.href = '/'} variant="outline">
31 | {t('home')}
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/[locale]/blog/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getLandingPage } from "@/services/page";
3 | import Header from "@/components/blocks/header";
4 | import Footer from "@/components/blocks/footer";
5 | import dynamic from 'next/dynamic';
6 |
7 | const SharedBackground = dynamic(() => import('@/components/ui/shared-background'), {
8 | ssr: false
9 | });
10 |
11 | export default async function BlogLayout({
12 | children,
13 | params: { locale },
14 | }: {
15 | children: React.ReactNode;
16 | params: { locale: string };
17 | }) {
18 | const page = await getLandingPage(locale);
19 |
20 | return (
21 | <>
22 |
23 | {page.header && }
24 | {children}
25 | {page.footer && }
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/[locale]/blog/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function BlogLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {[...Array(6)].map((_, i) => (
13 |
14 |
15 |
16 |
17 |
18 | ))}
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/[locale]/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function LocaleError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | // u8bb0u5f55u9519u8befu5230u9519u8befu62a5u544au670du52a1
18 | console.error(error);
19 | }, [error]);
20 |
21 | return (
22 |
23 |
{t('title')}
24 |
25 | {t('message')}
26 |
27 |
28 | reset()} variant="default">
29 | {t('retry')}
30 |
31 | {
33 | // 从URL路径中获取当前locale
34 | const pathParts = window.location.pathname.split('/');
35 | const locale = pathParts[1] || 'en'; // 默认英语
36 | window.location.href = `/${locale}`;
37 | }}
38 | variant="outline"
39 | >
40 | {t('home')}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/[locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/app/globals.css";
2 |
3 | import { getMessages, getTranslations } from "next-intl/server";
4 |
5 | import { AppContextProvider } from "@/contexts/app";
6 | import { Inter as FontSans } from "next/font/google";
7 | import { Metadata } from "next";
8 | import { NextAuthSessionProvider } from "@/auth/session";
9 | import { NextIntlClientProvider } from "next-intl";
10 | import { ThemeProvider } from "@/contexts/theme";
11 | import { AppShell } from "@/components/layout/app-shell";
12 | import { cn } from "@/lib/utils";
13 |
14 | const fontSans = FontSans({
15 | subsets: ["latin"],
16 | variable: "--font-sans",
17 | });
18 |
19 | export async function generateMetadata({
20 | params: { locale },
21 | }: {
22 | params: { locale: string };
23 | }): Promise {
24 | const t = await getTranslations();
25 |
26 | return {
27 | title: {
28 | template: `%s | ${t("metadata.title")}`,
29 | default: t("metadata.title") || "",
30 | },
31 | description: t("metadata.description") || "",
32 | keywords: t("metadata.keywords") || "",
33 | icons: {
34 | icon: [
35 | { url: "/favicon.ico", sizes: "any" },
36 | { url: "/favicon-16x16.png", sizes: "16x16" },
37 | { url: "/favicon-32x32.png", sizes: "32x32" }
38 | ],
39 | apple: "/apple-icon.png",
40 | shortcut: "/shortcut-icon.png"
41 | },
42 | manifest: "/site.webmanifest",
43 | };
44 | }
45 |
46 | export default async function LocaleLayout({
47 | children,
48 | params: { locale },
49 | }: Readonly<{
50 | children: React.ReactNode;
51 | params: { locale: string };
52 | }>) {
53 | const messages = await getMessages();
54 |
55 | console.log('Current locale:', locale);
56 | console.log('Available messages:', Object.keys(messages));
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
70 | {children}
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/[locale]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function LocaleLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/[locale]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { Button } from '@/components/ui/button';
3 | import { useTranslations } from 'next-intl';
4 |
5 | export default function LocaleNotFound() {
6 | const t = useTranslations('NotFound');
7 |
8 | return (
9 |
10 |
404
11 |
{t('title')}
12 |
13 | {t('message')}
14 |
15 |
16 | {t('home')}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/[locale]/pay-success/[session_id]/page.tsx:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 | import { handleOrderSession } from "@/services/order";
3 | import { redirect } from "next/navigation";
4 |
5 | export default async function ({ params }: { params: { session_id: string } }) {
6 | try {
7 | const stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY || "");
8 | const session = await stripe.checkout.sessions.retrieve(params.session_id);
9 |
10 | await handleOrderSession(session);
11 |
12 | redirect(process.env.NEXT_PUBLIC_PAY_SUCCESS_URL || "/");
13 | } catch (e) {
14 | redirect(process.env.NEXT_PUBLIC_PAY_FAIL_URL || "/");
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/[locale]/pay-success/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import { useTranslations } from 'next-intl';
6 |
7 | export default function PaySuccessError({
8 | error,
9 | reset,
10 | }: {
11 | error: Error & { digest?: string };
12 | reset: () => void;
13 | }) {
14 | const t = useTranslations('Error');
15 |
16 | useEffect(() => {
17 | console.error(error);
18 | }, [error]);
19 |
20 | return (
21 |
22 |
{t('title')}
23 |
24 | {t('message')}
25 |
26 |
27 | reset()} variant="default">
28 | {t('retry')}
29 |
30 | window.location.href = '/'} variant="outline">
31 | {t('home')}
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/[locale]/pay-success/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function PaySuccessLoading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/auth";
2 |
3 | export const { GET, POST } = handlers;
4 |
--------------------------------------------------------------------------------
/src/app/api/error.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | export function GET() {
4 | return NextResponse.json(
5 | { error: 'Method not allowed' },
6 | { status: 405 }
7 | );
8 | }
9 |
10 | export function POST() {
11 | return NextResponse.json(
12 | { error: 'Method not allowed' },
13 | { status: 405 }
14 | );
15 | }
16 |
17 | export function handleApiError(error: Error) {
18 | console.error('API Error:', error);
19 |
20 | return NextResponse.json(
21 | { error: 'Internal Server Error' },
22 | { status: 500 }
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/api/ping/route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreditsAmount,
3 | CreditsTransType,
4 | decreaseCredits,
5 | } from "@/services/credit";
6 | import { respData, respErr } from "@/lib/resp";
7 |
8 | import { getUserUuid } from "@/services/user";
9 |
10 | export async function POST(req: Request) {
11 | try {
12 | const { message } = await req.json();
13 | if (!message) {
14 | return respErr("invalid params");
15 | }
16 |
17 | const user_uuid = await getUserUuid();
18 | if (!user_uuid) {
19 | return respErr("no auth");
20 | }
21 |
22 | // decrease credits for ping
23 | await decreaseCredits({
24 | user_uuid,
25 | trans_type: CreditsTransType.Ping,
26 | credits: CreditsAmount.PingCost,
27 | });
28 |
29 | return respData({
30 | pong: `received message: ${message}`,
31 | });
32 | } catch (e) {
33 | console.log("test failed:", e);
34 | return respErr("test failed");
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/api/stripe-notify/route.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 | import { handleOrderSession } from "@/services/order";
3 | import { respOk } from "@/lib/resp";
4 |
5 | export async function POST(req: Request) {
6 | try {
7 | const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY;
8 | const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
9 |
10 | if (!stripePrivateKey || !stripeWebhookSecret) {
11 | throw new Error("invalid stripe config");
12 | }
13 |
14 | const stripe = new Stripe(stripePrivateKey);
15 |
16 | const sign = req.headers.get("stripe-signature") as string;
17 | const body = await req.text();
18 | if (!sign || !body) {
19 | throw new Error("invalid notify data");
20 | }
21 |
22 | const event = await stripe.webhooks.constructEventAsync(
23 | body,
24 | sign,
25 | stripeWebhookSecret
26 | );
27 |
28 | console.log("stripe notify event: ", event);
29 |
30 | switch (event.type) {
31 | case "checkout.session.completed": {
32 | const session = event.data.object;
33 |
34 | await handleOrderSession(session);
35 | break;
36 | }
37 |
38 | default:
39 | console.log("not handle event: ", event.type);
40 | }
41 |
42 | return respOk();
43 | } catch (e: any) {
44 | console.log("stripe notify failed: ", e);
45 | return Response.json(
46 | { error: `handle stripe notify failed: ${e.message}` },
47 | { status: 500 }
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 |
6 | export default function Error({
7 | error,
8 | reset,
9 | }: {
10 | error: Error & { digest?: string };
11 | reset: () => void;
12 | }) {
13 | useEffect(() => {
14 | // 记录错误到错误报告服务
15 | console.error(error);
16 | }, [error]);
17 |
18 | return (
19 |
20 |
出现了一些问题
21 |
22 | 很抱歉,我们遇到了一个错误。请尝试刷新页面或返回首页。
23 |
24 |
25 | reset()} variant="default">
26 | 重试
27 |
28 | {
30 | const locale = navigator.language.split('-')[0] || 'en';
31 | window.location.href = `/${locale}`;
32 | }}
33 | variant="outline"
34 | >
35 | 返回首页
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { Button } from '@/components/ui/button';
5 |
6 | export default function GlobalError({
7 | error,
8 | reset,
9 | }: {
10 | error: Error & { digest?: string };
11 | reset: () => void;
12 | }) {
13 | useEffect(() => {
14 | // 记录错误到错误报告服务
15 | console.error(error);
16 | }, [error]);
17 |
18 | return (
19 |
20 |
21 |
22 |
出现了严重问题
23 |
24 | 很抱歉,我们遇到了一个严重错误。请尝试刷新页面或返回首页。
25 |
26 |
27 | reset()} variant="default">
28 | 重试
29 |
30 | {
32 | const locale = navigator.language.split('-')[0] || 'en';
33 | window.location.href = `/${locale}`;
34 | }}
35 | variant="outline"
36 | >
37 | 返回首页
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 | import { Inter } from 'next/font/google';
3 | import { getLocale } from 'next-intl/server';
4 |
5 | const inter = Inter({ subsets: ['latin'] });
6 |
7 | export const metadata = {
8 | title: 'AI Wallpaper Generator',
9 | description: 'Generate beautiful AI wallpapers with a simple prompt',
10 | };
11 |
12 | export default async function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | // 统一从服务端获取 locale,避免客户端水合时缺失 params 导致不一致
18 | const locale = (await getLocale())?.toLowerCase();
19 | const lang = locale && locale.startsWith('zh') ? 'zh-CN' : 'en';
20 |
21 | return (
22 |
23 |
24 | {children}
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/not-found.global.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import { Button } from '@/components/ui/button';
5 |
6 | export default function GlobalNotFound() {
7 | return (
8 |
9 |
404
10 |
u9875u9762u672au627eu5230
11 |
12 | u5f88u62b1u6b49uff0cu60a8u8bbfu95eeu7684u9875u9762u4e0du5b58u5728u6216u5df2u88abu79fbu9664u3002
13 |
14 |
{
16 | // 使用浏览器语言或默认语言
17 | const locale = navigator.language.split('-')[0] || 'en';
18 | window.location.href = `/${locale}`;
19 | }}
20 | variant="default"
21 | >
22 | 返回首页
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import { useRouter } from 'next/navigation';
5 |
6 | export default function RootPage() {
7 | const router = useRouter();
8 |
9 | useEffect(() => {
10 | // 获取浏览器语言
11 | const browserLang = navigator.language.split('-')[0];
12 | // 只接受我们支持的语言,否则使用默认英语
13 | const locale = ['en', 'zh'].includes(browserLang) ? browserLang : 'en';
14 |
15 | // 重定向到本地化主页
16 | router.replace(`/${locale}`);
17 | }, [router]);
18 |
19 | // 渲染一个简单的加载状态,很快会被重定向
20 | return (
21 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/template.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect } from 'react';
4 |
5 | export default function Template({ children }: { children: React.ReactNode }) {
6 | const [mounted, setMounted] = useState(false);
7 |
8 | // 确保组件仅在客户端渲染
9 | useEffect(() => {
10 | setMounted(true);
11 | }, []);
12 |
13 | if (!mounted) {
14 | return null;
15 | }
16 |
17 | return (
18 | <>
19 | {children}
20 | >
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/auth/index.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { authOptions } from "./config";
3 |
4 | export const { handlers, signIn, signOut, auth } = NextAuth(authOptions);
5 |
--------------------------------------------------------------------------------
/src/auth/session.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SessionProvider } from "next-auth/react";
4 |
5 | export function NextAuthSessionProvider({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | return {children} ;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/blocks/blog-detail/crumb.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Post } from "@/types/post";
4 | import { motion } from "@/components/ui/motion";
5 | import { ChevronRight, Home } from "lucide-react";
6 | import Link from "next/link";
7 |
8 | export default function Crumb({ post }: { post: Post }) {
9 | return (
10 |
11 |
17 |
18 |
22 |
23 | Home
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 | 博客
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {post.title}
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/blocks/crumb/index.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronRight } from "lucide-react";
2 | import Link from "next/link";
3 | import { NavItem } from "@/types/blocks/base";
4 |
5 | export default function Crumb({ items }: { items: NavItem[] }) {
6 | return (
7 |
8 | {items.map((item, index) => {
9 | const isActive = item.is_active;
10 | return (
11 |
12 |
18 | {item.title}
19 |
20 |
21 | {!isActive && (
22 |
23 | )}
24 |
25 | );
26 | })}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/blocks/empty/index.tsx:
--------------------------------------------------------------------------------
1 | export default function ({ message }: { message: string }) {
2 | return (
3 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/blocks/footer/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Footer as FooterType } from "@/types/blocks/footer";
4 | import { useTranslations } from "next-intl";
5 | import { motion } from "framer-motion";
6 |
7 | export default function Footer({ footer }: { footer: FooterType }) {
8 | const t = useTranslations("footer");
9 |
10 | if (footer.disabled) {
11 | return null;
12 | }
13 |
14 | return (
15 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/blocks/hero/announcement-bar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion } from "@/components/ui/motion";
4 | import { Star } from "lucide-react";
5 |
6 | interface AnnouncementProps {
7 | label?: string;
8 | title?: string;
9 | }
10 |
11 | export function AnnouncementBar({ label, title }: AnnouncementProps) {
12 | if (!label && !title) return null;
13 |
14 | return (
15 |
21 |
22 | {label && (
23 |
24 | {label}
25 |
26 | )}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {title}
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/blocks/hero/floating-image.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion } from '@/components/ui/motion';
4 | import { useState, useEffect } from 'react';
5 | import Image from 'next/image';
6 |
7 | interface FloatingImageProps {
8 | src: string;
9 | alt: string;
10 | width: number;
11 | height: number;
12 | className?: string;
13 | delay?: number;
14 | amplitude?: number; // 振幅大小
15 | duration?: number; // 动画周期
16 | }
17 |
18 | export default function FloatingImage({
19 | src,
20 | alt,
21 | width,
22 | height,
23 | className = '',
24 | delay = 0,
25 | amplitude = 10,
26 | duration = 6
27 | }: FloatingImageProps) {
28 | const [mounted, setMounted] = useState(false);
29 |
30 | useEffect(() => {
31 | setMounted(true);
32 | }, []);
33 |
34 | if (!mounted) return null;
35 |
36 | return (
37 |
60 |
61 |
68 |
69 |
70 | {/* Reflection effect */}
71 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/blocks/hero/happy-users.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarImage } from "@/components/ui/avatar";
2 |
3 | import { Star } from "lucide-react";
4 |
5 | export default function HappyUsers() {
6 | return (
7 |
8 |
9 | {Array.from({ length: 5 }).map((_, index) => (
10 |
11 |
15 |
16 | ))}
17 |
18 |
19 |
20 | {Array.from({ length: 5 }).map((_, index) => (
21 |
25 | ))}
26 |
27 |
28 | from 99+ happy users
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/blocks/table/copy.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CopyToClipboard } from "react-copy-to-clipboard";
4 | import { ReactNode } from "react";
5 | import { toast } from "sonner";
6 |
7 | export default function ({
8 | text,
9 | children,
10 | }: {
11 | text: string;
12 | children: ReactNode;
13 | }) {
14 | return (
15 | toast.success("Copied")}>
16 | {children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/blocks/table/dropdown.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuRadioGroup,
8 | DropdownMenuRadioItem,
9 | DropdownMenuSeparator,
10 | DropdownMenuShortcut,
11 | DropdownMenuSub,
12 | DropdownMenuSubContent,
13 | DropdownMenuSubTrigger,
14 | DropdownMenuTrigger,
15 | } from "@/components/ui/dropdown-menu";
16 |
17 | import { Button } from "@/components/ui/button";
18 | import Icon from "@/components/icon";
19 | import Link from "next/link";
20 | import { MoreHorizontal } from "lucide-react";
21 | import { NavItem } from "@/types/blocks/base";
22 |
23 | export default function ({ items }: { items: NavItem[] }) {
24 | return (
25 |
26 |
27 |
31 |
32 | Open menu
33 |
34 |
35 |
36 | {items.map((item) => {
37 | return (
38 |
39 |
44 | {item.icon && }
45 | {item.title}
46 |
47 |
48 | );
49 | })}
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/blocks/table/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Table,
3 | TableBody,
4 | TableCaption,
5 | TableCell,
6 | TableFooter,
7 | TableHead,
8 | TableHeader,
9 | TableRow,
10 | } from "@/components/ui/table";
11 |
12 | import Copy from "./copy";
13 | import { CopyToClipboard } from "react-copy-to-clipboard";
14 | import { TableColumn } from "@/types/blocks/table";
15 |
16 | export default function ({
17 | columns,
18 | data,
19 | empty_message,
20 | }: {
21 | columns?: TableColumn[];
22 | data?: any[];
23 | empty_message?: string;
24 | }) {
25 | if (!columns) {
26 | columns = [];
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 | {columns &&
34 | columns.map((item: TableColumn, idx: number) => {
35 | return (
36 |
37 | {item.title}
38 |
39 | );
40 | })}
41 |
42 |
43 |
44 | {data && data.length > 0 ? (
45 | data.map((item: any, idx: number) => (
46 |
47 | {columns &&
48 | columns.map((column: TableColumn, iidx: number) => {
49 | const content = column.callback
50 | ? column.callback(item)
51 | : item[column.name as keyof typeof item];
52 | const value = item[column.name as keyof typeof item];
53 |
54 | if (column.type === "copy" && value) {
55 | return (
56 |
57 | {content}
58 |
59 | );
60 | }
61 |
62 | return (
63 |
64 | {content}
65 |
66 | );
67 | })}
68 |
69 | ))
70 | ) : (
71 |
72 |
73 |
74 |
{empty_message}
75 |
76 |
77 |
78 | )}
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/blocks/toolbar/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Button as ButtonType } from "@/types/blocks/base";
3 | import Icon from "@/components/icon";
4 | import Link from "next/link";
5 |
6 | export default function Toolbar({ items }: { items?: ButtonType[] }) {
7 | return (
8 |
9 | {items?.map((item, idx) => (
10 |
16 |
21 | {item.title}
22 | {item.icon && }
23 |
24 |
25 | ))}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/console/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { Sidebar } from "@/types/blocks/sidebar";
3 | import SidebarNav from "@/components/console/sidebar/nav";
4 |
5 | export default async function ConsoleLayout({
6 | children,
7 | sidebar,
8 | }: {
9 | children: ReactNode;
10 | sidebar?: Sidebar;
11 | }) {
12 | return (
13 |
14 |
15 |
16 | {sidebar?.nav?.items && (
17 |
20 | )}
21 |
{children}
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/console/sidebar/nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Icon from "@/components/icon";
4 | import Link from "next/link";
5 | import { NavItem } from "@/types/blocks/base";
6 | import { buttonVariants } from "@/components/ui/button";
7 | import { cn } from "@/lib/utils";
8 | import { usePathname } from "next/navigation";
9 |
10 | export default function ({
11 | className,
12 | items,
13 | ...props
14 | }: {
15 | className?: string;
16 | items: NavItem[];
17 | }) {
18 | const pathname = usePathname();
19 |
20 | return (
21 |
28 | {items.map((item, index) => (
29 |
40 | {item.icon && }
41 | {item.title}
42 |
43 | ))}
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/console/slots/form/index.tsx:
--------------------------------------------------------------------------------
1 | import Crumb from "@/components/blocks/crumb";
2 | import FormBlock from "@/components/blocks/form";
3 | import { Form as FormSlotType } from "@/types/slots/form";
4 | import { Separator } from "@/components/ui/separator";
5 | import Toolbar from "@/components/blocks/toolbar";
6 |
7 | export default function ({ ...form }: FormSlotType) {
8 | return (
9 |
10 | {form.crumb?.items &&
}
11 |
12 |
{form.title}
13 |
{form.description}
14 |
15 | {form.tip && (
16 |
17 | {form.tip.description || form.tip.title}
18 |
19 | )}
20 | {form.toolbar &&
}
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/console/slots/table/index.tsx:
--------------------------------------------------------------------------------
1 | import { Separator } from "@/components/ui/separator";
2 | import TableBlock from "@/components/blocks/table";
3 | import { Table as TableSlotType } from "@/types/slots/table";
4 | import Toolbar from "@/components/blocks/toolbar";
5 |
6 | export default function ({ ...table }: TableSlotType) {
7 | return (
8 |
9 |
10 |
{table.title}
11 |
{table.description}
12 |
13 | {table.tip && (
14 |
15 | {table.tip.description || table.tip.title}
16 |
17 | )}
18 | {table.toolbar &&
}
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/dashboard/header/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Breadcrumb,
3 | BreadcrumbList,
4 | BreadcrumbPage,
5 | BreadcrumbSeparator,
6 | } from "@/components/ui/breadcrumb";
7 |
8 | import { BreadcrumbItem } from "@/components/ui/breadcrumb";
9 | import { BreadcrumbLink } from "@/components/ui/breadcrumb";
10 | import { Crumb } from "@/types/blocks/base";
11 | import { Fragment } from "react";
12 | import Link from "next/link";
13 | import { Separator } from "@/components/ui/separator";
14 | import { SidebarTrigger } from "@/components/ui/sidebar";
15 |
16 | export default function ({ crumb }: { crumb?: Crumb }) {
17 | return (
18 |
19 |
20 |
21 | {crumb && crumb.items && crumb.items.length > 0 && (
22 | <>
23 |
24 |
25 |
26 | {crumb.items.map((item, index) => {
27 | if (item.is_active) {
28 | return (
29 |
30 | {item.title}
31 |
32 | );
33 | }
34 |
35 | return (
36 |
37 |
38 |
42 | {item.title}
43 |
44 |
45 |
46 |
47 | );
48 | })}
49 |
50 |
51 | >
52 | )}
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
2 |
3 | import { ReactNode } from "react";
4 | import Sidebar from "@/components/dashboard/sidebar";
5 | import { Sidebar as SidebarType } from "@/types/blocks/sidebar";
6 |
7 | export default async function DashboardLayout({
8 | children,
9 | sidebar,
10 | }: {
11 | children: ReactNode;
12 | sidebar?: SidebarType;
13 | }) {
14 | return (
15 |
16 | {sidebar && }
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/dashboard/sidebar/footer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar";
4 |
5 | import Icon from "@/components/icon";
6 | import Link from "next/link";
7 | import { Social as SocialType } from "@/types/blocks/base";
8 |
9 | export default function ({ social }: { social: SocialType }) {
10 | const { open } = useSidebar();
11 |
12 | const handleTabChange = (value: string) => {
13 | console.log(value);
14 | };
15 |
16 | return (
17 | <>
18 | {open ? (
19 |
20 | {social?.items?.map((item, idx: number) => (
21 |
22 |
23 | {item.icon && }
24 |
25 |
26 | ))}
27 |
28 | ) : null}
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/dashboard/sidebar/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SidebarMenu, SidebarMenuItem } from "@/components/ui/sidebar";
4 |
5 | import { Brand as BrandType } from "@/types/blocks/base";
6 | import { DropdownMenu } from "@/components/ui/dropdown-menu";
7 | import Link from "next/link";
8 |
9 | export default function ({ brand }: { brand: BrandType }) {
10 | return (
11 |
12 |
13 |
14 |
18 |
19 |
24 |
25 |
26 | {brand?.title}
27 |
28 | {/* {open && } */}
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/dashboard/sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Sidebar,
3 | SidebarContent,
4 | SidebarFooter,
5 | SidebarHeader,
6 | SidebarRail,
7 | } from "@/components/ui/sidebar";
8 |
9 | import Footer from "./footer";
10 | import Header from "./header";
11 | import Nav from "./nav";
12 | import { Sidebar as SidebarType } from "@/types/blocks/sidebar";
13 | import User from "./user";
14 |
15 | export default function ({ sidebar }: { sidebar: SidebarType }) {
16 | if (sidebar.disabled) {
17 | return null;
18 | }
19 |
20 | return (
21 |
22 | {sidebar?.brand && (
23 |
24 |
25 |
26 | )}
27 |
28 |
29 | {sidebar?.nav && }
30 | {sidebar?.library && sidebar.library}
31 |
32 |
33 |
34 | {sidebar?.social && }
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/dashboard/slots/form/index.tsx:
--------------------------------------------------------------------------------
1 | import FormBlock from "@/components/blocks/form";
2 | import { Form as FormSlotType } from "@/types/slots/form";
3 | import Header from "@/components/dashboard/header";
4 |
5 | export default function ({ ...form }: FormSlotType) {
6 | return (
7 | <>
8 |
9 |
10 |
{form.title}
11 |
12 |
19 |
20 |
21 | >
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/dashboard/slots/table/index.tsx:
--------------------------------------------------------------------------------
1 | import Header from "@/components/dashboard/header";
2 | import TableBlock from "@/components/blocks/table";
3 | import { Table as TableSlotType } from "@/types/slots/table";
4 | import Toolbar from "@/components/blocks/toolbar";
5 |
6 | export default function ({ ...table }: TableSlotType) {
7 | return (
8 | <>
9 |
10 |
11 |
{table.title}
12 | {table.description && (
13 |
14 | {table.description}
15 |
16 | )}
17 | {table.tip && (
18 |
19 | {table.tip.description || table.tip.title}
20 |
21 | )}
22 | {table.toolbar &&
}
23 |
26 |
27 | >
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/gallery/gallery.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Avatar, AvatarFallback } from "@/components/ui/avatar"
3 | import { Card, CardContent, CardFooter } from "@/components/ui/card"
4 |
5 | interface Image {
6 | id: string
7 | title: string
8 | resolution: string
9 | image: string
10 | avatar: string
11 | avatarColor: string
12 | }
13 |
14 | interface ImageGalleryProps {
15 | providedImage?: string
16 | }
17 |
18 | const defaultImages: Image[] = [
19 | {
20 | id: "1",
21 | title: "u6625u8282u5f20u706fu7ed3u5f69",
22 | image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-1ZIQEMRPl8vbvqd8P6cJsIwlKRcyN1.png",
23 | resolution: "1792u00d71024",
24 | avatar: "n",
25 | avatarColor: "bg-green-600",
26 | },
27 | {
28 | id: "2",
29 | title: "u5317u56fdu98ceu5149",
30 | image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-1ZIQEMRPl8vbvqd8P6cJsIwlKRcyN1.png",
31 | resolution: "1792u00d71024",
32 | avatar: "h",
33 | avatarColor: "bg-red-700",
34 | },
35 | {
36 | id: "3",
37 | title: "u5927u6f20u5b64u70dfu76f4 u957fu6cb3u843du65e5u5706",
38 | image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-1ZIQEMRPl8vbvqd8P6cJsIwlKRcyN1.png",
39 | resolution: "1792u00d71024",
40 | avatar: "u",
41 | avatarColor: "bg-purple-600",
42 | },
43 | ]
44 |
45 | export default function ImageGallery({ providedImage }: ImageGalleryProps) {
46 | const images = providedImage
47 | ? defaultImages.map((image) => ({ ...image, image: providedImage }))
48 | : defaultImages
49 |
50 | return (
51 |
52 |
53 |
54 | {images.map((image) => (
55 |
56 |
57 |
58 |
63 |
64 |
65 |
66 |
67 |
{image.title}
68 |
{image.resolution}
69 |
70 |
71 | {image.avatar}
72 |
73 |
74 |
75 | ))}
76 |
77 |
78 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/layout/app-shell.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode } from "react";
4 | import { Toaster } from "@/components/ui/sonner";
5 | import SignModal from "@/components/sign/modal";
6 |
7 | /**
8 | * u5e94u7528u5916u58f3u7ec4u4ef6
9 | * u96c6u6210u5168u5c40UIu7ec4u4ef6uff0cu5982u901au77e5u3001u767bu5f55u6a21u6001u6846
10 | */
11 | export function AppShell({ children }: { children: ReactNode }) {
12 | return (
13 | <>
14 | {children}
15 |
16 | {/* u5168u5c40UIu7ec4u4ef6 */}
17 |
18 |
19 | >
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/locale/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectItem,
7 | SelectTrigger,
8 | } from "@/components/ui/select";
9 | import { useParams, usePathname, useRouter } from "next/navigation";
10 |
11 | import { MdLanguage } from "react-icons/md";
12 | import { localeNames } from "@/i18n/locale";
13 | import { useTranslations } from "next-intl";
14 |
15 | export default function ({ isIcon = false }: { isIcon?: boolean }) {
16 | const params = useParams();
17 | const locale = params.locale as string;
18 | const router = useRouter();
19 | const pathname = usePathname();
20 | const t = useTranslations();
21 |
22 | const handleSwitchLanguage = (value: string) => {
23 | if (value !== locale) {
24 | let newPathName = pathname.replace(`/${locale}`, `/${value}`);
25 | if (!newPathName.startsWith(`/${value}`)) {
26 | newPathName = `/${value}${newPathName}`;
27 | }
28 | router.push(newPathName);
29 | }
30 | };
31 |
32 | return (
33 |
34 |
37 |
38 | {!isIcon && (
39 | {localeNames[locale]}
40 | )}
41 |
42 |
43 | {Object.keys(localeNames).map((key: string) => {
44 | const name = localeNames[key];
45 | return (
46 |
47 | {name}
48 |
49 | );
50 | })}
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/markdown/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import "highlight.js/styles/atom-one-dark.min.css";
4 | import "./markdown.css";
5 |
6 | import MarkdownIt from "markdown-it";
7 | import React from "react";
8 | import hljs from "highlight.js";
9 |
10 | export default function Markdown({ content }: { content: string }) {
11 | const md: MarkdownIt = new MarkdownIt({
12 | highlight: function (str: string, lang: string) {
13 | if (lang && hljs.getLanguage(lang)) {
14 | try {
15 | return `${
16 | hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
17 | } `;
18 | } catch (_) {}
19 | }
20 |
21 | return `${md.utils.escapeHtml(str)} `;
22 | },
23 | });
24 |
25 | const renderedMarkdown = md.render(content);
26 |
27 | return (
28 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/markdown/markdown.css:
--------------------------------------------------------------------------------
1 | .markdown {
2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
3 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
4 | sans-serif;
5 | line-height: 1.6;
6 | }
7 |
8 | .markdown h1,
9 | .markdown h2,
10 | .markdown h3,
11 | .markdown h4,
12 | .markdown h5,
13 | .markdown h6 {
14 | margin-top: 24px;
15 | margin-bottom: 16px;
16 | font-weight: 600;
17 | line-height: 1.25;
18 | }
19 |
20 | .markdown h1 {
21 | font-size: 2em;
22 | }
23 | .markdown h2 {
24 | font-size: 1.5em;
25 | }
26 | .markdown h3 {
27 | font-size: 1.25em;
28 | }
29 |
30 | .markdown p {
31 | margin-bottom: 16px;
32 | }
33 |
34 | .markdown a {
35 | color: #3182ce;
36 | text-decoration: none;
37 | }
38 |
39 | .markdown a:hover {
40 | text-decoration: underline;
41 | }
42 |
43 | .markdown pre {
44 | margin-bottom: 16px;
45 | padding: 16px;
46 | overflow: auto;
47 | font-size: 85%;
48 | line-height: 1.45;
49 | /* background-color: #1f2937; */
50 | border-radius: 6px;
51 | }
52 |
53 | .markdown code {
54 | font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
55 | font-size: 85%;
56 | }
57 |
58 | .markdown ul,
59 | .markdown ol {
60 | padding-left: 1em;
61 | margin-bottom: 16px;
62 | list-style-type: square;
63 | }
64 |
65 | .markdown img {
66 | max-width: 100%;
67 | box-sizing: border-box;
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/sign/sign_in.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { useAppContext } from "@/contexts/app";
5 | import { useTranslations } from "next-intl";
6 |
7 | export default function SignIn() {
8 | const t = useTranslations();
9 | const { setShowSignModal } = useAppContext();
10 |
11 | return (
12 | setShowSignModal(true)}
16 | >
17 | {t("user.sign_in")}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/sign/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import SignIn from "./sign_in";
4 | import User from "./user";
5 | import { useAppContext } from "@/contexts/app";
6 | import { useTranslations } from "next-intl";
7 |
8 | export default function SignToggle() {
9 | const t = useTranslations();
10 | const { user } = useAppContext();
11 |
12 | return (
13 |
14 | {user ? : }
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/sign/user.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuCheckboxItem,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuLabel,
12 | DropdownMenuSeparator,
13 | DropdownMenuTrigger,
14 | } from "@/components/ui/dropdown-menu";
15 |
16 | import Link from "next/link";
17 | import { User } from "@/types/user";
18 | import { signOut } from "next-auth/react";
19 | import { useTranslations } from "next-intl";
20 |
21 | export default function ({ user }: { user: User }) {
22 | const t = useTranslations();
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | {user.nickname}
30 |
31 |
32 |
33 |
34 | {user.nickname}
35 |
36 |
37 |
38 |
39 | {t("user.my_orders")}
40 |
41 |
42 |
43 |
44 | {t("my_credits.title")}
45 |
46 |
47 |
48 |
49 | {t("api_keys.title")}
50 |
51 |
52 |
53 | signOut()}
56 | >
57 | {t("user.sign_out")}
58 |
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 |
15 | /**
16 | * 主题切换按钮组件
17 | * 允许用户在明亮模式和黑暗模式之间切换
18 | */
19 | export function ThemeToggle() {
20 | const { setTheme, theme } = useTheme();
21 |
22 | return (
23 |
24 |
25 |
30 |
31 |
32 | Toggle theme
33 |
34 |
35 |
36 | setTheme("light")}
38 | className="focus:bg-primary focus:text-primary-foreground dark:focus:bg-accent dark:focus:text-accent-foreground"
39 | >
40 | Light
41 |
42 | setTheme("dark")}
44 | className="focus:bg-primary focus:text-primary-foreground dark:focus:bg-accent dark:focus:text-accent-foreground"
45 | >
46 | Dark
47 |
48 | setTheme("system")}
50 | className="focus:bg-primary focus:text-primary-foreground dark:focus:bg-accent dark:focus:text-accent-foreground"
51 | >
52 | System
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
59 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/src/components/ui/icon.tsx:
--------------------------------------------------------------------------------
1 | import { icons } from "lucide-react";
2 |
3 | export const Icon = ({
4 | name,
5 | color,
6 | size,
7 | className,
8 | }: {
9 | name: keyof typeof icons;
10 | color: string;
11 | size: number;
12 | className?: string;
13 | }) => {
14 | const LucideIcon = icons[name as keyof typeof icons];
15 |
16 | return ;
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/motion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | motion as framerMotion,
5 | AnimatePresence as FramerAnimatePresence,
6 | HTMLMotionProps
7 | } from "framer-motion";
8 | import { ReactNode } from "react";
9 |
10 | // 重新导出 framer-motion 组件,确保它们在客户端组件中使用
11 | export const motion = framerMotion;
12 | export const AnimatePresence = FramerAnimatePresence;
13 |
14 | // 预定义一些常用的动画变体
15 | export const fadeIn = {
16 | initial: { opacity: 0 },
17 | animate: { opacity: 1 },
18 | exit: { opacity: 0 }
19 | };
20 |
21 | export const slideUp = {
22 | initial: { opacity: 0, y: 20 },
23 | animate: { opacity: 1, y: 0 },
24 | exit: { opacity: 0, y: 20 }
25 | };
26 |
27 | export const slideIn = {
28 | initial: { opacity: 0, x: -20 },
29 | animate: { opacity: 1, x: 0 },
30 | exit: { opacity: 0, x: -20 }
31 | };
32 |
33 | export const stagger = {
34 | animate: {
35 | transition: {
36 | staggerChildren: 0.1
37 | }
38 | }
39 | };
40 |
41 | // 为动画组件定义基本props类型
42 | interface AnimationProps extends HTMLMotionProps<"div"> {
43 | children: ReactNode;
44 | delay?: number;
45 | duration?: number;
46 | className?: string;
47 | }
48 |
49 | // 创建一些常用的动画组件
50 | export const FadeIn = ({
51 | children,
52 | delay = 0,
53 | duration = 0.5,
54 | ...props
55 | }: AnimationProps) => {
56 | return (
57 |
64 | {children}
65 |
66 | );
67 | };
68 |
69 | export const SlideUp = ({
70 | children,
71 | delay = 0,
72 | duration = 0.5,
73 | ...props
74 | }: AnimationProps) => {
75 | return (
76 |
83 | {children}
84 |
85 | );
86 | };
87 |
88 | export const ScaleIn = ({
89 | children,
90 | delay = 0,
91 | duration = 0.5,
92 | ...props
93 | }: AnimationProps) => {
94 | return (
95 |
102 | {children}
103 |
104 | );
105 | };
106 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/ui/shared-background.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState } from 'react';
4 | import dynamic from 'next/dynamic';
5 |
6 | // 动态导入粒子背景组件以避免SSR问题
7 | const ParticlesBackground = dynamic(() =>
8 | import('@/components/blocks/hero/particles-background'), {
9 | ssr: false,
10 | loading: () =>
11 | });
12 |
13 | export const SharedBackground = () => {
14 | const [mounted, setMounted] = useState(false);
15 |
16 | useEffect(() => {
17 | setMounted(true);
18 | }, []);
19 |
20 | return (
21 |
22 | {/* 主要背景颜色和渐变 */}
23 |
30 |
31 | {/* 粒子背景 */}
32 | {mounted &&
}
33 |
34 | {/* 网格背景 */}
35 |
36 |
37 | {/* 装饰性光效 */}
38 |
39 |
40 |
41 | {/* 添加全局样式 */}
42 | {mounted && (
43 |
66 | )}
67 |
68 | );
69 | };
70 |
71 | export default SharedBackground;
72 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/contexts/app.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | ReactNode,
5 | createContext,
6 | useContext,
7 | useEffect,
8 | useState,
9 | } from "react";
10 |
11 | import { ContextValue } from "@/types/context";
12 | import { User } from "@/types/user";
13 | import useOneTapLogin from "@/hooks/useOneTapLogin";
14 | import { useSession } from "next-auth/react";
15 |
16 | const AppContext = createContext({} as ContextValue);
17 |
18 | export const useAppContext = () => useContext(AppContext);
19 |
20 | export const AppContextProvider = ({ children }: { children: ReactNode }) => {
21 | if (
22 | process.env.NEXT_PUBLIC_AUTH_GOOGLE_ONE_TAP_ENABLED === "true" &&
23 | process.env.NEXT_PUBLIC_AUTH_GOOGLE_ID
24 | ) {
25 | useOneTapLogin();
26 | }
27 |
28 | const { data: session } = useSession();
29 |
30 | //
31 | const [theme, setTheme] = useState("dark");
32 | const [showSignModal, setShowSignModal] = useState(false);
33 | const [user, setUser] = useState(null);
34 |
35 | useEffect(() => {
36 | if (session && session.user) {
37 | setUser(session.user);
38 | }
39 | }, [session]);
40 |
41 | return (
42 |
52 | {children}
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/contexts/theme.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode } from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import type { ThemeProviderProps } from "next-themes";
6 |
7 | /**
8 | * 主题提供者组件
9 | * 提供应用的主题管理功能,使用next-themes实现
10 | * 支持明亮和黑暗模式切换
11 | */
12 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
13 | return (
14 |
15 | {children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/data/install.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE users (
2 | id SERIAL PRIMARY KEY,
3 | uuid VARCHAR(255) UNIQUE NOT NULL,
4 | email VARCHAR(255) NOT NULL,
5 | created_at timestamptz,
6 | nickname VARCHAR(255),
7 | avatar_url VARCHAR(255),
8 | locale VARCHAR(50),
9 | signin_type VARCHAR(50),
10 | signin_ip VARCHAR(255),
11 | signin_provider VARCHAR(50),
12 | signin_openid VARCHAR(255),
13 | UNIQUE (email, signin_provider)
14 | );
15 |
16 | CREATE TABLE orders (
17 | id SERIAL PRIMARY KEY,
18 | order_no VARCHAR(255) UNIQUE NOT NULL,
19 | created_at timestamptz,
20 | user_uuid VARCHAR(255) NOT NULL DEFAULT '',
21 | user_email VARCHAR(255) NOT NULL DEFAULT '',
22 | amount INT NOT NULL,
23 | interval VARCHAR(50),
24 | expired_at timestamptz,
25 | status VARCHAR(50) NOT NULL,
26 | stripe_session_id VARCHAR(255),
27 | credits INT NOT NULL,
28 | currency VARCHAR(50),
29 | sub_id VARCHAR(255),
30 | sub_interval_count int,
31 | sub_cycle_anchor int,
32 | sub_period_end int,
33 | sub_period_start int,
34 | sub_times int,
35 | product_id VARCHAR(255),
36 | product_name VARCHAR(255),
37 | valid_months int,
38 | order_detail TEXT,
39 | paid_at timestamptz,
40 | paid_email VARCHAR(255),
41 | paid_detail TEXT
42 | );
43 |
44 |
45 | CREATE TABLE apikeys (
46 | id SERIAL PRIMARY KEY,
47 | api_key VARCHAR(255) UNIQUE NOT NULL,
48 | title VARCHAR(100),
49 | user_uuid VARCHAR(255) NOT NULL,
50 | created_at timestamptz,
51 | status VARCHAR(50)
52 | );
53 |
54 | CREATE TABLE credits (
55 | id SERIAL PRIMARY KEY,
56 | trans_no VARCHAR(255) UNIQUE NOT NULL,
57 | created_at timestamptz,
58 | user_uuid VARCHAR(255) NOT NULL,
59 | trans_type VARCHAR(50) NOT NULL,
60 | credits INT NOT NULL,
61 | order_no VARCHAR(255),
62 | expired_at timestamptz
63 | );
64 |
65 | CREATE TABLE posts (
66 | id SERIAL PRIMARY KEY,
67 | uuid VARCHAR(255) UNIQUE NOT NULL,
68 | slug VARCHAR(255),
69 | title VARCHAR(255),
70 | description TEXT,
71 | content TEXT,
72 | created_at timestamptz,
73 | updated_at timestamptz,
74 | status VARCHAR(50),
75 | cover_url VARCHAR(255),
76 | author_name VARCHAR(255),
77 | author_avatar_url VARCHAR(255),
78 | locale VARCHAR(50)
79 | );
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useMediaQuery.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function useMediaQuery(query: string): boolean {
4 | const [matches, setMatches] = useState(false);
5 |
6 | useEffect(() => {
7 | const mediaQuery = window.matchMedia(query);
8 |
9 | // Initial check
10 | setMatches(mediaQuery.matches);
11 |
12 | // Create a callback function to handle changes
13 | const handleChange = (event: MediaQueryListEvent) => {
14 | setMatches(event.matches);
15 | };
16 |
17 | // Add the listener
18 | mediaQuery.addEventListener("change", handleChange);
19 |
20 | // Clean up
21 | return () => {
22 | mediaQuery.removeEventListener("change", handleChange);
23 | };
24 | }, [query]);
25 |
26 | return matches;
27 | }
28 |
--------------------------------------------------------------------------------
/src/hooks/useOneTapLogin.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import googleOneTap from "google-one-tap";
4 | import { signIn } from "next-auth/react";
5 | import { useEffect } from "react";
6 | import { useSession } from "next-auth/react";
7 |
8 | export default function () {
9 | const { data: session, status } = useSession();
10 |
11 | const oneTapLogin = async function () {
12 | const options = {
13 | client_id: process.env.NEXT_PUBLIC_AUTH_GOOGLE_ID,
14 | auto_select: false,
15 | cancel_on_tap_outside: false,
16 | context: "signin",
17 | };
18 |
19 | // console.log("onetap login trigger", options);
20 |
21 | googleOneTap(options, (response: any) => {
22 | console.log("onetap login ok", response);
23 | handleLogin(response.credential);
24 | });
25 | };
26 |
27 | const handleLogin = async function (credentials: string) {
28 | const res = await signIn("google-one-tap", {
29 | credential: credentials,
30 | redirect: false,
31 | });
32 | console.log("signIn ok", res);
33 | };
34 |
35 | useEffect(() => {
36 | // console.log("one tap login status", status, session);
37 |
38 | if (status === "unauthenticated") {
39 | oneTapLogin();
40 |
41 | const intervalId = setInterval(() => {
42 | oneTapLogin();
43 | }, 3000);
44 |
45 | return () => {
46 | clearInterval(intervalId);
47 | };
48 | }
49 | }, [status]);
50 |
51 | return <>>;
52 | }
53 |
--------------------------------------------------------------------------------
/src/i18n.ts:
--------------------------------------------------------------------------------
1 | import { getRequestConfig } from 'next-intl/server';
2 |
3 | export default getRequestConfig(async ({ locale }: { locale: string }) => {
4 | // 加载当前语言的消息
5 | const messages = (await import(`./i18n/messages/${locale}.json`)).default;
6 |
7 | return {
8 | messages,
9 | };
10 | });
11 |
--------------------------------------------------------------------------------
/src/i18n/locale.ts:
--------------------------------------------------------------------------------
1 | import { Pathnames } from "next-intl/routing";
2 |
3 | export const locales = ["en", "zh"];
4 |
5 | export const localeNames: any = {
6 | en: "English",
7 | zh: "中文",
8 | };
9 |
10 | export const defaultLocale = "en";
11 |
12 | export const localePrefix = "as-needed";
13 |
14 | export const localeDetection =
15 | process.env.NEXT_PUBLIC_LOCALE_DETECTION === "true";
16 |
17 | export const pathnames = {
18 | en: {
19 | "privacy-policy": "/privacy-policy",
20 | "terms-of-service": "/terms-of-service",
21 | },
22 | } satisfies Pathnames;
23 |
--------------------------------------------------------------------------------
/src/i18n/request.ts:
--------------------------------------------------------------------------------
1 | import { getRequestConfig } from "next-intl/server";
2 | import { routing } from "./routing";
3 |
4 | export default getRequestConfig(async ({ requestLocale }) => {
5 | let locale = await requestLocale;
6 | if (!locale || !routing.locales.includes(locale as any)) {
7 | locale = routing.defaultLocale;
8 | }
9 |
10 | if (["zh-CN"].includes(locale)) {
11 | locale = "zh";
12 | }
13 |
14 | if (!routing.locales.includes(locale as any)) {
15 | locale = "en";
16 | }
17 |
18 | try {
19 | const messages = (await import(`./messages/${locale.toLowerCase()}.json`))
20 | .default;
21 | return {
22 | locale: locale,
23 | messages: messages,
24 | };
25 | } catch (e) {
26 | return {
27 | locale: "en",
28 | messages: (await import(`./messages/en.json`)).default,
29 | };
30 | }
31 | });
32 |
--------------------------------------------------------------------------------
/src/i18n/routing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultLocale,
3 | localeDetection,
4 | localePrefix,
5 | locales,
6 | pathnames,
7 | } from "./locale";
8 |
9 | import { createSharedPathnamesNavigation } from "next-intl/navigation";
10 | import { defineRouting } from "next-intl/routing";
11 |
12 | export const routing = defineRouting({
13 | locales,
14 | defaultLocale,
15 | localePrefix,
16 | pathnames,
17 | localeDetection,
18 | });
19 |
20 | export const { Link, redirect, usePathname, useRouter } =
21 | createSharedPathnamesNavigation(routing);
22 |
--------------------------------------------------------------------------------
/src/i18n/utils.ts:
--------------------------------------------------------------------------------
1 | import { getTranslations as getNextIntlTranslations } from 'next-intl/server';
2 |
3 | /**
4 | * 获取翻译函数,用于服务器端API路由
5 | * @param locale 语言代码
6 | * @returns 翻译函数
7 | */
8 | export async function getTranslations(locale: string) {
9 | // 默认使用英文
10 | const finalLocale = locale || 'en';
11 |
12 | // 获取翻译函数
13 | const messages = await import(`./messages/${finalLocale}.json`);
14 |
15 | // 简单包装翻译函数,处理无翻译键的情况
16 | return function translate(key: string, params?: Record) {
17 | try {
18 | // 分割键路径
19 | const parts = key.split('.');
20 | let current: any = messages;
21 |
22 | // 遍历路径查找翻译
23 | for (const part of parts) {
24 | if (current[part] === undefined) {
25 | // 如果翻译不存在,返回键名
26 | console.warn(`Translation key not found: ${key}`);
27 | return key;
28 | }
29 | current = current[part];
30 | }
31 |
32 | // 如果有参数,进行简单替换
33 | if (params && typeof current === 'string') {
34 | return Object.entries(params).reduce((result, [key, value]) => {
35 | return result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
36 | }, current);
37 | }
38 |
39 | return current;
40 | } catch (error) {
41 | console.error(`Error in translation for key: ${key}`, error);
42 | return key;
43 | }
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/lib/cache.ts:
--------------------------------------------------------------------------------
1 | import { getTimestamp } from "./time";
2 |
3 | // get data from cache
4 | export const cacheGet = (key: string): string | null => {
5 | let valueWithExpires = localStorage.getItem(key);
6 | if (!valueWithExpires) {
7 | return null;
8 | }
9 |
10 | let valueArr = valueWithExpires.split(":");
11 | if (!valueArr || valueArr.length < 2) {
12 | return null;
13 | }
14 |
15 | const expiresAt = Number(valueArr[0]);
16 | const currTimestamp = getTimestamp();
17 |
18 | if (expiresAt !== -1 && expiresAt < currTimestamp) {
19 | // value expired
20 | cacheRemove(key);
21 |
22 | return null;
23 | }
24 |
25 | const searchStr = valueArr[0] + ":";
26 | const value = valueWithExpires.replace(searchStr, "");
27 |
28 | return value;
29 | };
30 |
31 | // set data to cache
32 | // expiresAt: absolute timestamp, -1 means no expire
33 | export const cacheSet = (key: string, value: string, expiresAt: number) => {
34 | const valueWithExpires = expiresAt + ":" + value;
35 |
36 | localStorage.setItem(key, valueWithExpires);
37 | };
38 |
39 | // remove data from cache
40 | export const cacheRemove = (key: string) => {
41 | localStorage.removeItem(key);
42 | };
43 |
44 | // clear all datas from cache
45 | export const cacheClear = () => {
46 | localStorage.clear();
47 | };
48 |
--------------------------------------------------------------------------------
/src/lib/hash.ts:
--------------------------------------------------------------------------------
1 | import { SnowflakeIdv1 } from "simple-flakeid";
2 | import { v4 as uuidv4 } from "uuid";
3 |
4 | export function getUuid(): string {
5 | return uuidv4();
6 | }
7 |
8 | export function getUniSeq(prefix: string = ""): string {
9 | const timestamp = Date.now().toString(36);
10 | const randomPart = Math.random().toString(36).substring(2, 8);
11 |
12 | return `${prefix}${randomPart}${timestamp}`;
13 | }
14 |
15 | export function getNonceStr(length: number): string {
16 | const characters =
17 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
18 | let result = "";
19 | const charactersLength = characters.length;
20 |
21 | for (let i = 0; i < length; i++) {
22 | const randomIndex = Math.floor(Math.random() * charactersLength);
23 | result += characters[randomIndex];
24 | }
25 |
26 | return result;
27 | }
28 |
29 | export function getSnowId(): string {
30 | const gen = new SnowflakeIdv1({ workerId: 1 });
31 | const snowId = gen.NextId();
32 |
33 | return snowId.toString();
34 | }
35 |
--------------------------------------------------------------------------------
/src/lib/ip.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { headers } from "next/headers";
4 |
5 | export async function getClientIp() {
6 | const h = headers();
7 |
8 | const ip =
9 | h.get("cf-connecting-ip") || // Cloudflare IP
10 | h.get("x-real-ip") || // Vercel or other reverse proxies
11 | (h.get("x-forwarded-for") || "127.0.0.1").split(",")[0]; // Standard header
12 |
13 | return ip;
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/resp.ts:
--------------------------------------------------------------------------------
1 | import { getTranslations } from 'next-intl/server';
2 |
3 | // 响应状态码常量
4 | const RESPONSE_STATUS = {
5 | SUCCESS: 0,
6 | ERROR: 1
7 | };
8 |
9 | export const respData = (data: T) => {
10 | return Response.json({ code: RESPONSE_STATUS.SUCCESS, data });
11 | };
12 |
13 | export const respOk = () => {
14 | return Response.json({ code: RESPONSE_STATUS.SUCCESS, message: "ok" });
15 | };
16 |
17 | export async function respErr(messageKey: string, locale: string = 'en') {
18 | try {
19 | // 尝试获取翻译
20 | const t = await getTranslations({ locale, namespace: 'api.errors' });
21 | const translatedMessage = t(messageKey, { fallback: messageKey });
22 |
23 | return Response.json({ code: RESPONSE_STATUS.ERROR, message: translatedMessage });
24 | } catch (error) {
25 | // 如果翻译失败,返回原始消息
26 | console.error('Translation error:', error);
27 | return Response.json({ code: RESPONSE_STATUS.ERROR, message: messageKey });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/time.ts:
--------------------------------------------------------------------------------
1 | export function getIsoTimestr(): string {
2 | return new Date().toISOString();
3 | }
4 |
5 | export const getTimestamp = () => {
6 | let time = Date.parse(new Date().toUTCString());
7 |
8 | return time / 1000;
9 | };
10 |
11 | export const getMillisecond = () => {
12 | let time = new Date().getTime();
13 |
14 | return time;
15 | };
16 |
17 | export const getOneYearLaterTimestr = () => {
18 | const currentDate = new Date();
19 | const oneYearLater = new Date(currentDate);
20 | oneYearLater.setFullYear(currentDate.getFullYear() + 1);
21 |
22 | return oneYearLater.toISOString();
23 | };
24 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import type { MDXComponents } from "mdx/types";
2 |
3 | export function useMDXComponents(components: MDXComponents): MDXComponents {
4 | return {
5 | ...components,
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import createMiddleware from "next-intl/middleware";
2 |
3 | // 简化路由配置,减少可能的错误
4 | export default createMiddleware({
5 | locales: ["en", "zh"],
6 | defaultLocale: "en",
7 | localePrefix: "as-needed"
8 | });
9 |
10 | export const config = {
11 | matcher: [
12 | "/",
13 | "/(en|zh)/:path*",
14 | "/((?!privacy-policy|terms-of-service|api|_next|_vercel|.*\\..*).*)",
15 | ],
16 | };
17 |
--------------------------------------------------------------------------------
/src/models/apikey.ts:
--------------------------------------------------------------------------------
1 | import { Apikey } from "@/types/apikey";
2 | import { getSupabaseClient } from "@/models/db";
3 |
4 | export enum ApikeyStatus {
5 | Created = "created",
6 | Deleted = "deleted",
7 | }
8 |
9 | export async function insertApikey(apikey: Apikey) {
10 | const supabase = getSupabaseClient();
11 | const { data, error } = await supabase.from("apikeys").insert(apikey);
12 |
13 | if (error) throw error;
14 |
15 | return data;
16 | }
17 |
18 | export async function getUserApikeys(
19 | user_uuid: string,
20 | page: number = 1,
21 | limit: number = 50
22 | ): Promise {
23 | const offset = (page - 1) * limit;
24 |
25 | const supabase = getSupabaseClient();
26 | const { data, error } = await supabase
27 | .from("apikeys")
28 | .select("*")
29 | .eq("user_uuid", user_uuid)
30 | .neq("status", ApikeyStatus.Deleted)
31 | .order("created_at", { ascending: false })
32 | .range(offset, offset + limit - 1);
33 |
34 | if (error) {
35 | return undefined;
36 | }
37 |
38 | return data;
39 | }
40 |
41 | export async function getUserUuidByApiKey(
42 | apiKey: string
43 | ): Promise {
44 | const supabase = getSupabaseClient();
45 | const { data, error } = await supabase
46 | .from("apikeys")
47 | .select("user_uuid")
48 | .eq("api_key", apiKey)
49 | .eq("status", ApikeyStatus.Created)
50 | .limit(1)
51 | .single();
52 |
53 | if (error) {
54 | return undefined;
55 | }
56 |
57 | return data?.user_uuid;
58 | }
59 |
--------------------------------------------------------------------------------
/src/models/credit.ts:
--------------------------------------------------------------------------------
1 | import { Credit } from "@/types/credit";
2 | import { getSupabaseClient } from "@/models/db";
3 |
4 | export async function insertCredit(credit: Credit) {
5 | const supabase = getSupabaseClient();
6 | const { data, error } = await supabase.from("credits").insert(credit);
7 |
8 | if (error) {
9 | throw error;
10 | }
11 |
12 | return data;
13 | }
14 |
15 | export async function findCreditByTransNo(
16 | trans_no: string
17 | ): Promise {
18 | const supabase = getSupabaseClient();
19 | const { data, error } = await supabase
20 | .from("credits")
21 | .select("*")
22 | .eq("trans_no", trans_no)
23 | .limit(1)
24 | .single();
25 |
26 | if (error) {
27 | return undefined;
28 | }
29 |
30 | return data;
31 | }
32 |
33 | export async function getUserValidCredits(
34 | user_uuid: string
35 | ): Promise {
36 | const now = new Date().toISOString();
37 | const supabase = getSupabaseClient();
38 | const { data, error } = await supabase
39 | .from("credits")
40 | .select("*")
41 | .eq("user_uuid", user_uuid)
42 | .gte("expired_at", now)
43 | .order("expired_at", { ascending: true });
44 |
45 | if (error) {
46 | return undefined;
47 | }
48 |
49 | return data;
50 | }
51 |
52 | export async function getCreditsByUserUuid(
53 | user_uuid: string,
54 | page: number = 1,
55 | limit: number = 50
56 | ): Promise {
57 | const supabase = getSupabaseClient();
58 | const { data, error } = await supabase
59 | .from("credits")
60 | .select("*")
61 | .eq("user_uuid", user_uuid)
62 | .order("created_at", { ascending: false })
63 | .range((page - 1) * limit, page * limit - 1);
64 |
65 | if (error) {
66 | return undefined;
67 | }
68 |
69 | return data;
70 | }
71 |
--------------------------------------------------------------------------------
/src/models/db.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@supabase/supabase-js";
2 |
3 | export function getSupabaseClient() {
4 | const supabaseUrl = process.env.SUPABASE_URL || "";
5 |
6 | let supabaseKey = process.env.SUPABASE_ANON_KEY || "";
7 | if (process.env.SUPABASE_SERVICE_ROLE_KEY) {
8 | supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
9 | }
10 |
11 | if (!supabaseUrl || !supabaseKey) {
12 | throw new Error("Supabase URL or key is not set");
13 | }
14 |
15 | const client = createClient(supabaseUrl, supabaseKey);
16 |
17 | return client;
18 | }
19 |
--------------------------------------------------------------------------------
/src/models/post.ts:
--------------------------------------------------------------------------------
1 | import { Post } from "@/types/post";
2 | import { getSupabaseClient } from "./db";
3 |
4 | export enum PostStatus {
5 | Created = "created",
6 | Deleted = "deleted",
7 | Online = "online",
8 | Offline = "offline",
9 | }
10 |
11 | export async function insertPost(post: Post) {
12 | const supabase = getSupabaseClient();
13 | const { data, error } = await supabase.from("posts").insert(post);
14 |
15 | if (error) {
16 | throw error;
17 | }
18 |
19 | return data;
20 | }
21 |
22 | export async function updatePost(uuid: string, post: Partial) {
23 | const supabase = getSupabaseClient();
24 | const { data, error } = await supabase
25 | .from("posts")
26 | .update(post)
27 | .eq("uuid", uuid);
28 |
29 | if (error) {
30 | throw error;
31 | }
32 |
33 | return data;
34 | }
35 |
36 | export async function findPostByUuid(uuid: string): Promise {
37 | const supabase = getSupabaseClient();
38 | const { data, error } = await supabase
39 | .from("posts")
40 | .select("*")
41 | .eq("uuid", uuid)
42 | .limit(1)
43 | .single();
44 |
45 | if (error) {
46 | return undefined;
47 | }
48 |
49 | return data;
50 | }
51 |
52 | export async function findPostBySlug(
53 | slug: string,
54 | locale: string
55 | ): Promise {
56 | const supabase = getSupabaseClient();
57 | const { data, error } = await supabase
58 | .from("posts")
59 | .select("*")
60 | .eq("slug", slug)
61 | .eq("locale", locale)
62 | .limit(1)
63 | .single();
64 |
65 | if (error) {
66 | return undefined;
67 | }
68 |
69 | return data;
70 | }
71 |
72 | export async function getAllPosts(
73 | page: number = 1,
74 | limit: number = 50
75 | ): Promise {
76 | const supabase = getSupabaseClient();
77 | const { data, error } = await supabase
78 | .from("posts")
79 | .select("*")
80 | .order("created_at", { ascending: false })
81 | .range((page - 1) * limit, page * limit - 1);
82 |
83 | if (error) {
84 | return [];
85 | }
86 |
87 | return data;
88 | }
89 |
90 | export async function getPostsByLocale(
91 | locale: string,
92 | page: number = 1,
93 | limit: number = 50
94 | ): Promise {
95 | const supabase = getSupabaseClient();
96 | const { data, error } = await supabase
97 | .from("posts")
98 | .select("*")
99 | .eq("locale", locale)
100 | .eq("status", PostStatus.Online)
101 | .order("created_at", { ascending: false })
102 | .range((page - 1) * limit, page * limit - 1);
103 |
104 | if (error) {
105 | return [];
106 | }
107 |
108 | return data;
109 | }
110 |
--------------------------------------------------------------------------------
/src/models/user.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@/types/user";
2 | import { getSupabaseClient } from "./db";
3 |
4 | export async function insertUser(user: User) {
5 | const supabase = getSupabaseClient();
6 | const { data, error } = await supabase.from("users").insert(user);
7 |
8 | if (error) {
9 | throw error;
10 | }
11 |
12 | return data;
13 | }
14 |
15 | export async function findUserByEmail(
16 | email: string
17 | ): Promise {
18 | const supabase = getSupabaseClient();
19 | const { data, error } = await supabase
20 | .from("users")
21 | .select("*")
22 | .eq("email", email)
23 | .limit(1)
24 | .single();
25 |
26 | if (error) {
27 | return undefined;
28 | }
29 |
30 | return data;
31 | }
32 |
33 | export async function findUserByUuid(uuid: string): Promise {
34 | const supabase = getSupabaseClient();
35 | const { data, error } = await supabase
36 | .from("users")
37 | .select("*")
38 | .eq("uuid", uuid)
39 | .single();
40 |
41 | if (error) {
42 | return undefined;
43 | }
44 |
45 | return data;
46 | }
47 |
48 | export async function getUsers(
49 | page: number = 1,
50 | limit: number = 50
51 | ): Promise {
52 | if (page < 1) page = 1;
53 | if (limit <= 0) limit = 50;
54 |
55 | const offset = (page - 1) * limit;
56 | const supabase = getSupabaseClient();
57 |
58 | const { data, error } = await supabase
59 | .from("users")
60 | .select("*")
61 | .order("created_at", { ascending: false })
62 | .range(offset, offset + limit - 1);
63 |
64 | if (error) {
65 | return undefined;
66 | }
67 |
68 | return data;
69 | }
70 |
--------------------------------------------------------------------------------
/src/providers/image/index.ts:
--------------------------------------------------------------------------------
1 | export * from './v1';
2 |
--------------------------------------------------------------------------------
/src/providers/image/v1/implementations/flux.ts:
--------------------------------------------------------------------------------
1 | import { ImageModel } from '../interface';
2 | import { ImageModelOptions, ImageModelWarning } from '../types';
3 | import { checkCredits, consumeCredits, OperationType } from '@/services/credit';
4 |
5 | interface FluxAPIResponse {
6 | images: Array<{
7 | url: string;
8 | b64_json?: string;
9 | }>;
10 | timings?: {
11 | total_time: number;
12 | };
13 | seed?: number;
14 | }
15 |
16 | export class FluxImageProvider implements ImageModel {
17 | readonly provider = 'flux';
18 | readonly modelId = 'black-forest-labs/FLUX.1-dev'; // 更新为 FLUX.1-dev 模型
19 | readonly maxImagesPerCall = 1;
20 | readonly creditsPerImage = 10;
21 |
22 | private readonly apiKey: string;
23 | private readonly apiEndpoint = 'https://api.siliconflow.cn/v1/images/generations';
24 |
25 | constructor() {
26 | this.apiKey = process.env.FLUX_API_KEY || '';
27 |
28 | if (!this.apiKey) {
29 | throw new Error('Flux API Token 缺失');
30 | }
31 | }
32 |
33 | async doGenerate(options: ImageModelOptions): Promise<{
34 | images: Array;
35 | warnings: Array;
36 | }> {
37 | // 使用积分检查函数
38 | await checkCredits(options.user_uuid, OperationType.IMAGE_GENERATION, this.provider);
39 |
40 | try {
41 | console.log('Calling Flux API with options:', {
42 | model: this.modelId,
43 | prompt: options.prompt,
44 | size: options.size
45 | });
46 |
47 | // 调用硅基流动 API
48 | const response = await fetch(this.apiEndpoint, {
49 | method: 'POST',
50 | headers: {
51 | 'Authorization': `Bearer ${this.apiKey}`,
52 | 'Content-Type': 'application/json'
53 | },
54 | body: JSON.stringify({
55 | model: this.modelId,
56 | prompt: options.prompt,
57 | seed: Math.floor(Math.random() * 9999999999),
58 | width: options.size?.width || 1024, // FLUX.1-dev 支持更高分辨率
59 | height: options.size?.height || 1024,
60 | num_images: 1,
61 | steps: 30,
62 | cfg_scale: 7.5,
63 | style_preset: "photographic",
64 | negative_prompt: "ugly, blurry, low quality, distorted, disfigured"
65 | })
66 | });
67 |
68 | if (!response.ok) {
69 | const errorText = await response.text();
70 | console.error('API Error:', errorText);
71 | throw new Error(`API 调用失败: ${response.statusText}. Details: ${errorText}`);
72 | }
73 |
74 | const result = await response.json() as FluxAPIResponse;
75 | console.log('Flux API Response:', result);
76 |
77 | // 使用积分扣除函数
78 | await consumeCredits(options.user_uuid, OperationType.IMAGE_GENERATION, this.provider);
79 |
80 | // 从响应中提取图片 URL
81 | const imageUrls = result.images.map(img => img.url);
82 |
83 | return {
84 | images: imageUrls,
85 | warnings: []
86 | };
87 | } catch (error: any) {
88 | console.error('Image generation error:', error);
89 | throw new Error(`图片生成失败: ${error.message}`);
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/providers/image/v1/index.ts:
--------------------------------------------------------------------------------
1 | export type { ImageModel } from './interface';
2 | export type { ImageModelOptions, ImageModelWarning } from './types';
3 | export { FluxImageProvider } from './implementations/flux';
4 |
--------------------------------------------------------------------------------
/src/providers/image/v1/interface.ts:
--------------------------------------------------------------------------------
1 | import { ImageModelOptions, ImageModelWarning } from "./types";
2 |
3 | export interface ImageModel {
4 | readonly provider: string;
5 | readonly modelId: string;
6 | readonly maxImagesPerCall: number;
7 | readonly creditsPerImage: number;
8 |
9 | doGenerate(options: ImageModelOptions): Promise<{
10 | images: Array;
11 | warnings: Array;
12 | }>;
13 | }
14 |
--------------------------------------------------------------------------------
/src/providers/image/v1/types.ts:
--------------------------------------------------------------------------------
1 | export type ImageModelOptions = {
2 | prompt: string;
3 | user_uuid: string;
4 | n?: number;
5 | size?: {
6 | width: number;
7 | height: number;
8 | };
9 | style?: string;
10 | };
11 |
12 | export type ImageModelWarning = {
13 | type: string;
14 | message: string;
15 | };
16 |
--------------------------------------------------------------------------------
/src/providers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './image';
2 |
--------------------------------------------------------------------------------
/src/services/apikey.ts:
--------------------------------------------------------------------------------
1 | export function getApiKey(req: Request) {
2 | const auth = req.headers.get("Authorization");
3 | if (!auth) {
4 | return "";
5 | }
6 |
7 | return auth.replace("Bearer ", "");
8 | }
9 |
--------------------------------------------------------------------------------
/src/services/constant.ts:
--------------------------------------------------------------------------------
1 | export const CacheKey = {
2 | Theme: "THEME",
3 | };
4 |
--------------------------------------------------------------------------------
/src/services/order.ts:
--------------------------------------------------------------------------------
1 | import { CreditsTransType, increaseCredits } from "./credit";
2 | import { findOrderByOrderNo, updateOrderStatus } from "@/models/order";
3 | import { getIsoTimestr, getOneYearLaterTimestr } from "@/lib/time";
4 |
5 | import Stripe from "stripe";
6 |
7 | export async function handleOrderSession(session: Stripe.Checkout.Session) {
8 | try {
9 | if (
10 | !session ||
11 | !session.metadata ||
12 | !session.metadata.order_no ||
13 | session.payment_status !== "paid"
14 | ) {
15 | throw new Error("invalid session");
16 | }
17 |
18 | const order_no = session.metadata.order_no;
19 | const paid_email =
20 | session.customer_details?.email || session.customer_email || "";
21 | const paid_detail = JSON.stringify(session);
22 |
23 | const order = await findOrderByOrderNo(order_no);
24 | if (!order || order.status !== "created") {
25 | throw new Error("invalid order");
26 | }
27 |
28 | const paid_at = getIsoTimestr();
29 | await updateOrderStatus(order_no, "paid", paid_at, paid_email, paid_detail);
30 |
31 | if (order.user_uuid && order.credits > 0) {
32 | // increase credits for paied order
33 | await increaseCredits({
34 | user_uuid: order.user_uuid,
35 | trans_type: CreditsTransType.OrderPay,
36 | credits: order.credits,
37 | expired_at: order.expired_at,
38 | order_no: order_no,
39 | });
40 | }
41 |
42 | console.log(
43 | "handle order session successed: ",
44 | order_no,
45 | paid_at,
46 | paid_email,
47 | paid_detail
48 | );
49 | } catch (e) {
50 | console.log("handle order session failed: ", e);
51 | throw e;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/services/page.ts:
--------------------------------------------------------------------------------
1 | import { LandingPage } from "@/types/pages/landing";
2 |
3 | export async function getLandingPage(locale: string): Promise {
4 | try {
5 | if (locale === "zh-CN") {
6 | locale = "zh";
7 | }
8 | return await import(
9 | `@/i18n/pages/landing/${locale.toLowerCase()}.json`
10 | ).then((module) => module.default);
11 | } catch (error) {
12 | console.warn(`Failed to load ${locale}.json, falling back to en.json`);
13 | return await import("@/i18n/pages/landing/en.json").then(
14 | (module) => module.default as LandingPage
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/services/user.ts:
--------------------------------------------------------------------------------
1 | import { CreditsAmount, CreditsTransType } from "./credit";
2 | import { findUserByEmail, findUserByUuid, insertUser } from "@/models/user";
3 |
4 | import { User } from "@/types/user";
5 | import { auth } from "@/auth";
6 | import { getOneYearLaterTimestr } from "@/lib/time";
7 | import { getUserUuidByApiKey } from "@/models/apikey";
8 | import { headers } from "next/headers";
9 | import { increaseCredits } from "./credit";
10 |
11 | export async function saveUser(user: User) {
12 | try {
13 | const existUser = await findUserByEmail(user.email);
14 | if (!existUser) {
15 | await insertUser(user);
16 |
17 | // increase credits for new user, expire in one year
18 | await increaseCredits({
19 | user_uuid: user.uuid || "",
20 | trans_type: CreditsTransType.NewUser,
21 | credits: CreditsAmount.NewUserGet,
22 | expired_at: getOneYearLaterTimestr(),
23 | });
24 | } else {
25 | user.id = existUser.id;
26 | user.uuid = existUser.uuid;
27 | user.created_at = existUser.created_at;
28 | }
29 |
30 | return user;
31 | } catch (e) {
32 | console.log("save user failed: ", e);
33 | throw e;
34 | }
35 | }
36 |
37 | export async function getUserUuid() {
38 | let user_uuid = "";
39 |
40 | const token = getBearerToken();
41 |
42 | if (token) {
43 | // api key
44 | if (token.startsWith("sk-")) {
45 | const user_uuid = await getUserUuidByApiKey(token);
46 |
47 | return user_uuid || "";
48 | }
49 | }
50 |
51 | const session = await auth();
52 | if (session && session.user && session.user.uuid) {
53 | user_uuid = session.user.uuid;
54 | }
55 |
56 | return user_uuid;
57 | }
58 |
59 | export function getBearerToken() {
60 | const h = headers();
61 | const auth = h.get("Authorization");
62 | if (!auth) {
63 | return "";
64 | }
65 |
66 | return auth.replace("Bearer ", "");
67 | }
68 |
69 | export async function getUserEmail() {
70 | let user_email = "";
71 |
72 | const session = await auth();
73 | if (session && session.user && session.user.email) {
74 | user_email = session.user.email;
75 | }
76 |
77 | return user_email;
78 | }
79 |
80 | export async function getUserInfo() {
81 | let user_uuid = await getUserUuid();
82 |
83 | if (!user_uuid) {
84 | return;
85 | }
86 |
87 | const user = await findUserByUuid(user_uuid);
88 |
89 | return user;
90 | }
91 |
--------------------------------------------------------------------------------
/src/types/apikey.d.ts:
--------------------------------------------------------------------------------
1 | export interface Apikey {
2 | api_key: string;
3 | title: string;
4 | user_uuid: string;
5 | created_at: string;
6 | status: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/blocks/base.d.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export type ButtonType = "button" | "link";
4 |
5 | export type ButtonVariant =
6 | | "secondary"
7 | | "link"
8 | | "default"
9 | | "destructive"
10 | | "outline"
11 | | "ghost"
12 | | null
13 | | undefined;
14 |
15 | export type ButtonSize = "sm" | "md" | "lg";
16 |
17 | export interface Button {
18 | title?: string;
19 | icon?: string;
20 | url?: string;
21 | target?: string;
22 | type?: ButtonType;
23 | variant?: ButtonVariant;
24 | size?: ButtonSize;
25 | className?: string;
26 | }
27 |
28 | export interface Image {
29 | src?: string;
30 | alt?: string;
31 | className?: string;
32 | }
33 |
34 | export interface Brand {
35 | title?: string;
36 | description?: string;
37 | logo?: Image;
38 | url?: string;
39 | target?: string;
40 | }
41 |
42 | export interface NavItem {
43 | name?: string;
44 | title?: string;
45 | description?: string;
46 | icon?: string;
47 | image?: Image;
48 | url?: string;
49 | target?: string;
50 | is_active?: boolean;
51 | is_expand?: boolean;
52 | className?: string;
53 | children?: NavItem[];
54 | }
55 |
56 | export interface Nav {
57 | name?: string;
58 | title?: string;
59 | icon?: string;
60 | image?: Image;
61 | className?: string;
62 | items?: NavItem[];
63 | }
64 |
65 | export interface Crumb {
66 | items?: NavItem[];
67 | }
68 |
69 | export interface Toolbar {
70 | items?: Button[];
71 | }
72 |
73 | export interface Tip {
74 | title?: string;
75 | description?: string;
76 | icon?: string;
77 | type?: "info" | "warning" | "error";
78 | }
79 |
80 | export interface SocialItem {
81 | title: string;
82 | icon?: string;
83 | url?: string;
84 | target?: string;
85 | }
86 |
87 | export interface Social {
88 | items?: SocialItem[];
89 | }
90 |
91 | export interface AgreementItem {
92 | title?: string;
93 | url?: string;
94 | target?: string;
95 | }
96 |
97 | export interface Agreement {
98 | items?: AgreementItem[];
99 | }
100 |
--------------------------------------------------------------------------------
/src/types/blocks/blog.d.ts:
--------------------------------------------------------------------------------
1 | import { Image } from "@/types/blocks/base";
2 |
3 | export interface BlogItem {
4 | slug?: string;
5 | title?: string;
6 | description?: string;
7 | author_name?: string;
8 | author_avatar_url?: string;
9 | created_at?: string;
10 | locale?: string;
11 | cover_url?: string;
12 | content?: string;
13 | url?: string;
14 | target?: string;
15 | }
16 |
17 | export interface Blog {
18 | disabled?: boolean;
19 | name?: string;
20 | title?: string;
21 | description?: string;
22 | label?: string;
23 | icon?: string;
24 | image?: Image;
25 | buttons?: Button[];
26 | items?: BlogItem[];
27 | read_more_text?: string;
28 | }
29 |
--------------------------------------------------------------------------------
/src/types/blocks/footer.d.ts:
--------------------------------------------------------------------------------
1 | import { Brand, Social, Nav, Agreement } from "@/types/blocks/base";
2 |
3 | export interface Footer {
4 | disabled?: boolean;
5 | name?: string;
6 | brand?: Brand;
7 | nav?: Nav;
8 | copyright?: string;
9 | social?: Social;
10 | agreement?: Agreement;
11 | }
12 |
--------------------------------------------------------------------------------
/src/types/blocks/form.d.ts:
--------------------------------------------------------------------------------
1 | import { Button } from "@/types/blocks/base";
2 |
3 | type ValidationRule = {
4 | required?: boolean;
5 | min?: number;
6 | max?: number;
7 | message?: string;
8 | email?: boolean;
9 | };
10 |
11 | export interface FormField {
12 | name?: string;
13 | title?: string;
14 | type?:
15 | | "text"
16 | | "textarea"
17 | | "number"
18 | | "email"
19 | | "password"
20 | | "select"
21 | | "url"
22 | | "code_editor"
23 | | "markdown_editor";
24 | placeholder?: string;
25 | options?: {
26 | title: string;
27 | value: string;
28 | }[];
29 | value?: string;
30 | tip?: string;
31 | attributes?: Record;
32 | validation?: ValidationRule;
33 | }
34 |
35 | export interface FormSubmit {
36 | button?: Button;
37 | handler?: (
38 | data: FormData,
39 | passby?: any
40 | ) => Promise<
41 | | {
42 | status: "success" | "error";
43 | message: string;
44 | redirect_url?: string;
45 | }
46 | | undefined
47 | | void
48 | >;
49 | }
50 |
51 | export interface Form {
52 | fields: FormField[];
53 | data?: any;
54 | submit?: FormSubmit;
55 | }
56 |
--------------------------------------------------------------------------------
/src/types/blocks/header.d.ts:
--------------------------------------------------------------------------------
1 | import { Button, Brand, Nav } from "@/types/blocks/base";
2 |
3 | export interface Header {
4 | disabled?: boolean;
5 | name?: string;
6 | brand?: Brand;
7 | nav?: Nav;
8 | buttons?: Button[];
9 | className?: string;
10 | show_sign?: boolean;
11 | show_locale?: boolean;
12 | show_theme?: boolean;
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/blocks/hero.d.ts:
--------------------------------------------------------------------------------
1 | import { Button, Image } from "@/types/blocks/base";
2 |
3 | export interface Announcement {
4 | title?: string;
5 | description?: string;
6 | label?: string;
7 | url?: string;
8 | target?: string;
9 | }
10 |
11 | export interface Button {
12 | title?: string;
13 | url?: string;
14 | target?: string;
15 | icon?: string;
16 | type?: 'primary' | 'secondary';
17 | }
18 |
19 | export interface Hero {
20 | name?: string;
21 | disabled?: boolean;
22 | announcement?: Announcement;
23 | title?: string;
24 | highlight_text?: string;
25 | description?: string;
26 | buttons?: Button[];
27 | image?: Image;
28 | tip?: string;
29 | show_happy_users?: boolean;
30 | show_badge?: boolean;
31 | }
32 |
--------------------------------------------------------------------------------
/src/types/blocks/pricing.d.ts:
--------------------------------------------------------------------------------
1 | import { Button } from "@/types/blocks/base/button";
2 |
3 | export interface PricingGroup {
4 | name?: string;
5 | title?: string;
6 | description?: string;
7 | label?: string;
8 | }
9 |
10 | export interface PricingItem {
11 | title?: string;
12 | description?: string;
13 | label?: string;
14 | price?: string;
15 | original_price?: string;
16 | currency?: string;
17 | unit?: string;
18 | features_title?: string;
19 | features?: string[];
20 | button?: Button;
21 | tip?: string;
22 | is_featured?: boolean;
23 | interval: "month" | "year" | "one-time";
24 | product_id: string;
25 | product_name?: string;
26 | amount: number;
27 | cn_amount?: number;
28 | currency: string;
29 | credits?: number;
30 | valid_months?: number;
31 | group?: string;
32 | }
33 |
34 | export interface Pricing {
35 | disabled?: boolean;
36 | name?: string;
37 | title?: string;
38 | description?: string;
39 | items?: PricingItem[];
40 | groups?: PricingGroup[];
41 | }
42 |
--------------------------------------------------------------------------------
/src/types/blocks/section.d.ts:
--------------------------------------------------------------------------------
1 | import { Image, Button } from "@/types/blocks/base";
2 |
3 | export interface SectionItem {
4 | title?: string;
5 | description?: string;
6 | label?: string;
7 | icon?: string;
8 | image?: Image;
9 | buttons?: Button[];
10 | url?: string;
11 | target?: string;
12 | children?: SectionItem[];
13 | }
14 |
15 | export interface Section {
16 | disabled?: boolean;
17 | name?: string;
18 | title?: string;
19 | description?: string;
20 | label?: string;
21 | icon?: string;
22 | image?: Image;
23 | buttons?: Button[];
24 | items?: SectionItem[];
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/blocks/sidebar.d.ts:
--------------------------------------------------------------------------------
1 | import { Brand, Nav, Social } from "@/types/blocks/base";
2 |
3 | export interface Sidebar {
4 | disabled?: boolean;
5 | brand?: Brand;
6 | nav?: Nav;
7 | library?: ReactNode;
8 | social?: Social;
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/blocks/table.d.ts:
--------------------------------------------------------------------------------
1 | export interface TableColumn {
2 | name?: string;
3 | title?: string;
4 | type?: string;
5 | options?: any[];
6 | className?: string;
7 | callback?: (item: any) => any;
8 | }
9 |
10 | export interface Table {
11 | columns: TableColumn[];
12 | data: any[];
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/context.d.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export interface ContextValue {
4 | [propName: string]: any;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/credit.d.ts:
--------------------------------------------------------------------------------
1 | export interface Credit {
2 | trans_no: string;
3 | created_at: string;
4 | user_uuid: string;
5 | trans_type: string;
6 | credits: number;
7 | order_no: string;
8 | expired_at?: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module "google-one-tap";
2 |
--------------------------------------------------------------------------------
/src/types/mdx.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.mdx" {
2 | import type { ReactElement } from "react";
3 | const content: (props: any) => ReactElement;
4 | export default content;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import "next-auth";
2 |
3 | declare module "next-auth" {
4 | interface JWT {
5 | user?: {
6 | uuid?: string;
7 | nickname?: string;
8 | avatar_url?: string;
9 | created_at?: string;
10 | };
11 | }
12 |
13 | interface Session {
14 | user: {
15 | uuid?: string;
16 | nickname?: string;
17 | avatar_url?: string;
18 | created_at?: string;
19 | } & DefaultSession["user"];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/types/order.d.ts:
--------------------------------------------------------------------------------
1 | export interface Order {
2 | order_no: string;
3 | created_at: string;
4 | user_uuid: string;
5 | user_email: string;
6 | amount: number;
7 | interval: string;
8 | expired_at: string;
9 | status: string;
10 | stripe_session_id?: string;
11 | credits: number;
12 | currency: string;
13 | sub_id?: string;
14 | sub_interval_count?: number;
15 | sub_cycle_anchor?: number;
16 | sub_period_end?: number;
17 | sub_period_start?: number;
18 | sub_times?: number;
19 | product_id?: string;
20 | product_name?: string;
21 | valid_months?: number;
22 | order_detail?: string;
23 | paid_at?: string;
24 | paid_email?: string;
25 | paid_detail?: string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/types/pages/landing.d.ts:
--------------------------------------------------------------------------------
1 | import { Header } from "@/types/blocks/header";
2 | import { Hero } from "@/types/blocks/hero";
3 | import { Section } from "@/types/blocks/section";
4 | import { Footer } from "@/types/blocks/footer";
5 |
6 | export interface LandingPage {
7 | header?: Header;
8 | hero?: Hero;
9 | branding?: Section;
10 | introduce?: Section;
11 | benefit?: Section;
12 | usage?: Section;
13 | feature?: Section;
14 | showcase?: Section;
15 | stats?: Section;
16 | pricing?: Pricing;
17 | testimonial?: Section;
18 | faq?: Section;
19 | cta?: Section;
20 | footer?: Footer;
21 | }
22 |
--------------------------------------------------------------------------------
/src/types/post.d.ts:
--------------------------------------------------------------------------------
1 | export interface Post {
2 | uuid?: string;
3 | slug?: string;
4 | title?: string;
5 | description?: string;
6 | content?: string;
7 | created_at?: string;
8 | updated_at?: string;
9 | status?: string;
10 | cover_url?: string;
11 | author_name?: string;
12 | author_avatar_url?: string;
13 | locale?: string;
14 | reading_time?: number;
15 | related_posts?: Post[];
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/slots/base.d.ts:
--------------------------------------------------------------------------------
1 | import { Crumb, Toolbar, Tip } from "@/types/blocks/base";
2 |
3 | export interface Slot {
4 | title?: string;
5 | description?: string;
6 | tip?: Tip;
7 | crumb?: Crumb;
8 | toolbar?: Toolbar;
9 | loading?: boolean;
10 | data?: any;
11 | passby?: any;
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/slots/form.d.ts:
--------------------------------------------------------------------------------
1 | import { FormField, FormSubmit } from "@/types/blocks/form";
2 | import { Slot } from "@/types/slots/base";
3 |
4 | export interface Form extends Slot {
5 | fields?: FormField[];
6 | submit?: FormSubmit;
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/slots/table.d.ts:
--------------------------------------------------------------------------------
1 | import { TableColumn } from "@/types/blocks/table";
2 | import { Slot } from "@/types/slots/base";
3 |
4 | export interface Table extends Slot {
5 | columns?: TableColumn[];
6 | empty_message?: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/user.d.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id?: number;
3 | uuid?: string;
4 | email: string;
5 | created_at?: string;
6 | nickname: string;
7 | avatar_url: string;
8 | locale?: string;
9 | signin_type?: string;
10 | signin_ip?: string;
11 | signin_provider?: string;
12 | signin_openid?: string;
13 | credits?: UserCredits;
14 | }
15 |
16 | export interface UserCredits {
17 | one_time_credits?: number;
18 | monthly_credits?: number;
19 | total_credits?: number;
20 | used_credits?: number;
21 | left_credits: number;
22 | free_credits?: number;
23 | is_recharged?: boolean;
24 | is_pro?: boolean;
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "framework": "nextjs",
3 | "functions": {
4 | "app/api/**/*": {
5 | "maxDuration": 60
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------