├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── ci.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── README_CN.md
├── SECURITY.md
├── app
├── dashboard
│ ├── keys
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── page.tsx
│ └── settings
│ │ └── page.tsx
├── globals.css
├── layout.tsx
├── login
│ └── page.tsx
├── page.tsx
└── register
│ └── layout.tsx
├── components.json
├── components
├── api-balance-card.tsx
├── api-key-list.tsx
├── dashboard-layout.tsx
├── error-notification.tsx
├── language-switcher.tsx
├── login-form.tsx
├── settings-form.tsx
├── theme-provider.tsx
└── ui
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── alert.tsx
│ ├── aspect-ratio.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── breadcrumb.tsx
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── card.tsx
│ ├── carousel.tsx
│ ├── chart.tsx
│ ├── checkbox.tsx
│ ├── collapsible.tsx
│ ├── command.tsx
│ ├── context-menu.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── form.tsx
│ ├── hover-card.tsx
│ ├── input-otp.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── menubar.tsx
│ ├── navigation-menu.tsx
│ ├── pagination.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── radio-group.tsx
│ ├── resizable.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── sidebar.tsx
│ ├── skeleton.tsx
│ ├── slider.tsx
│ ├── sonner.tsx
│ ├── switch.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ ├── toggle-group.tsx
│ ├── toggle.tsx
│ ├── tooltip.tsx
│ ├── use-mobile.tsx
│ └── use-toast.ts
├── hooks
├── use-mobile.tsx
└── use-toast.ts
├── lib
├── actions
│ └── auth-actions.ts
├── api-connection.ts
├── api-key-storage.ts
├── encryption.ts
├── error-handler.ts
├── i18n
│ ├── language-context.tsx
│ └── translations.ts
├── storage.ts
├── storage
│ └── settings-storage.ts
└── utils.ts
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── public
├── image
│ ├── Dashboard-cn.png
│ ├── Dashboard.png
│ ├── api-key-cn.png
│ ├── api-key.png
│ ├── complex-cn.png
│ ├── newkey-complex.png
│ ├── newkey.png
│ ├── newkeyi-cn.png
│ ├── sys-set-cn.png
│ └── sys-set.png
├── placeholder-logo.png
├── placeholder-logo.svg
├── placeholder-user.jpg
├── placeholder.jpg
└── placeholder.svg
├── styles
└── globals.css
├── tailwind.config.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: '[BUG] '
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Environment (please complete the following information):**
27 | - OS: [e.g. Windows, macOS]
28 | - Browser: [e.g. Chrome, Safari]
29 | - Version: [e.g. 22]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: '[FEATURE] '
5 | labels: 'enhancement'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
22 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
3 |
4 | Fixes # (issue)
5 |
6 | ## Type of change
7 | Please delete options that are not relevant.
8 |
9 | - [ ] Bug fix (non-breaking change which fixes an issue)
10 | - [ ] New feature (non-breaking change which adds functionality)
11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12 | - [ ] Documentation update
13 |
14 | ## How Has This Been Tested?
15 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
16 |
17 | ## Checklist:
18 | - [ ] My code follows the style guidelines of this project
19 | - [ ] I have performed a self-review of my own code
20 | - [ ] I have commented my code, particularly in hard-to-understand areas
21 | - [ ] I have made corresponding changes to the documentation
22 | - [ ] My changes generate no new warnings
23 | - [ ] I have added tests that prove my fix is effective or that my feature works
24 | - [ ] New and existing unit tests pass locally with my changes
25 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [18.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | # 移除缓存选项,因为没有锁文件
25 | # cache: 'npm'
26 |
27 | - name: Generate package-lock.json
28 | run: npm install --package-lock-only
29 |
30 | - name: Install dependencies
31 | run: npm ci || npm install
32 | # npm ci 需要 package-lock.json,如果失败则回退到 npm install
33 |
34 | - name: Build
35 | run: npm run build
36 | env:
37 | CI: true
38 |
39 | - name: Lint
40 | run: npm run lint || echo "Linting skipped"
41 |
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # next.js
10 | /.next/
11 | /out/
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # local env files
26 | .env*.local
27 | .env
28 |
29 | # vercel
30 | .vercel
31 |
32 | # typescript
33 | *.tsbuildinfo
34 | next-env.d.ts
35 |
36 | # IDE
37 | .idea
38 | .vscode/*
39 | !.vscode/extensions.json
40 | !.vscode/settings.json
41 |
42 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to NanMeng API Key Manager
2 |
3 | Thank you for considering contributing to the NanMeng API Key Manager! This document provides guidelines and instructions for contributing.
4 |
5 | ## Code of Conduct
6 |
7 | By participating in this project, you agree to abide by our Code of Conduct. Please be respectful and considerate of others.
8 |
9 | ## How Can I Contribute?
10 |
11 | ### Reporting Bugs
12 |
13 | - Check if the bug has already been reported in the Issues section
14 | - Use the bug report template when creating a new issue
15 | - Include detailed steps to reproduce the bug
16 | - Include screenshots if applicable
17 | - Describe what you expected to happen and what actually happened
18 |
19 | ### Suggesting Enhancements
20 |
21 | - Check if the enhancement has already been suggested in the Issues section
22 | - Use the feature request template when creating a new issue
23 | - Provide a clear description of the enhancement
24 | - Explain why this enhancement would be useful to most users
25 |
26 | ### Pull Requests
27 |
28 | 1. Fork the repository
29 | 2. Create a new branch (`git checkout -b feature/your-feature-name`)
30 | 3. Make your changes
31 | 4. Run tests if available
32 | 5. Commit your changes (`git commit -m 'Add some feature'`)
33 | 6. Push to the branch (`git push origin feature/your-feature-name`)
34 | 7. Open a Pull Request
35 |
36 | ## Development Setup
37 |
38 | 1. Clone the repository
39 | 2. Install dependencies with `npm install`
40 | 3. Run the development server with `npm run dev`
41 | 4. Visit `http://localhost:3000` to see the application
42 |
43 | ## Coding Guidelines
44 |
45 | - Follow the existing code style
46 | - Write clear, readable, and maintainable code
47 | - Add comments for complex logic
48 | - Update documentation when necessary
49 |
50 | ## Commit Message Guidelines
51 |
52 | - Use the present tense ("Add feature" not "Added feature")
53 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
54 | - Limit the first line to 72 characters or less
55 | - Reference issues and pull requests liberally after the first line
56 |
57 | Thank you for contributing!
58 |
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 NanMeng
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 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # API Key Manager
2 |
3 |
4 |
5 |
6 |
7 |
8 | English |
9 | 简体中文
10 |
11 |
12 | A secure and efficient API key management system that helps developers and teams easily manage API keys for various AI models.
13 |
14 | ## Preface
15 | The API Key Manager aims to provide AI developers and enterprises with a one-stop solution for API key management. In today's diverse AI service environment, managing API keys from multiple providers is both cumbersome and poses security risks. This system helps users efficiently organize and utilize various service keys through secure encrypted storage, status monitoring, and convenient management features, reducing the risk of leakage and improving development efficiency. The system employs local storage technology, ensuring your sensitive key information is not uploaded to the cloud, further enhancing data security and privacy protection. Whether you are an individual developer or an enterprise user, the API Key Manager can meet your API key management needs. Of course, not only can it manage AI-related keys, but other related keys can also be uniformly managed using this manager.
16 |
17 | For example, when using the [dify](https://github.com/langgenius/dify) project, you need to fill in multiple API keys for large AI models, which can be a headache. This is also one of the starting points of this project.
18 |
19 | ## ✨ Features
20 |
21 | - [x] 🔑 **API Key Management**: Securely store and manage API keys from multiple AI service providers
22 | - [x] 🔒 **Security Encryption**: Protect your API keys with advanced encryption technology
23 | - [x] ⚡ **Status Monitoring**: Monitor API key connection status in real time
24 | - [x] 🌐 **Multi-language Support**: Switch between Chinese and English interfaces
25 | - [x] ⚙️ **Custom Settings**: Customize system settings and preferences according to your needs
26 | - [x] 🔄 **Connection Testing**: Test API connections directly from the dashboard
27 | - [ ] 📊 **Usage Monitoring**: Monitor API key usage and token consumption (Coming soon)
28 |
29 | ## 📋 Table of Contents
30 |
31 | - [Features](#-features)
32 | - [Demo](#-demo)
33 | - [Installation](#-installation)
34 | - [Vercel Deployment](#vercel-deployment)
35 | - [Local Installation](#local-installation)
36 | - [Docker Deployment](#docker-deployment)
37 | - [Usage](#-usage)
38 | - [Configuration](#-configuration)
39 |
40 | ## 📸 Demo
41 |
42 | Visit our [demo site](https://www.a888.online) to experience the application.
43 |
44 | API Key Management Page: Key Overview
45 |
46 | 
47 |
48 | Adding Keys: Basic Key + Composite Key
49 |
50 |  
51 |
52 | Dashboard: Monitor Connection Status
53 |
54 | 
55 |
56 | System Settings: Password Changes and Other Operations
57 |
58 | 
59 |
60 |
61 | Default login credentials:
62 | - Username: `admin`
63 | - Password: `password`
64 |
65 | After logging in, it is recommended to change the password and remember it for long-term use.
66 |
67 | ## 📥 Installation
68 |
69 | ### Vercel Deployment
70 |
71 | Deploying the NanMeng API Key Manager with Vercel is the simplest method:
72 |
73 | 1. Fork this repository to your GitHub account
74 | 2. Sign up or log in to [Vercel](https://vercel.com)
75 | 3. Click "New Project" and import your forked repository
76 | 4. Configure the following environment variables:
77 | - `ENCRYPTION_KEY`: A secure random string for encrypting API keys
78 | - `ALLOW_REGISTRATION`: Set to "true" or "false" to enable/disable user registration (registration feature has been deprecated)
79 | 5. Click "Deploy"
80 |
81 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fyourusername%2Fapi-key-manager)
82 |
83 | ### Local Installation
84 |
85 | #### Prerequisites
86 |
87 | - Node.js 18.x or higher
88 | - npm or yarn
89 | - Git
90 |
91 | #### Steps
92 |
93 | 1. Clone the repository:
94 | ```bash
95 | git clone https://github.com/randomAndre/api-key-manager.git
96 | cd api-key-manager
97 | ```
98 | 2. **Install Dependencies**
99 |
100 | ```bash
101 | npm install
102 | # or
103 | yarn install
104 | ```
105 |
106 |
107 | 3. **Environment Setup**
108 |
109 | Create a `.env.local` file in the root directory with the following variables:
110 |
111 | ```plaintext
112 | ENCRYPTION_KEY=your-secure-random-string
113 | ```
114 |
115 |
116 | 4. **Development Mode**
117 |
118 | To run the application in development mode:
119 |
120 | ```bash
121 | npm run dev
122 | # or
123 | yarn dev
124 | ```
125 |
126 | The application will be available at `http://localhost:3000`.
127 |
128 |
129 | 5. **Production Build**
130 |
131 | To create a production build:
132 |
133 | ```bash
134 | npm run build
135 | npm start
136 | # or
137 | yarn build
138 | yarn start
139 | ```
140 |
141 |
142 | 6. ### Docker Deployment
143 |
144 | You can also deploy using Docker:
145 |
146 | ```bash
147 | # Build the Docker image
148 | docker build -t nanmeng-api-key-manager .
149 |
150 | # Run the container
151 | docker run -p 3000:3000 -e ENCRYPTION_KEY=your-secure-key -e ALLOW_REGISTRATION=false nanmeng-api-key-manager
152 | ```
153 |
154 |
155 |
156 |
157 | ## 🚀 Usage
158 |
159 | ### First-time Setup
160 |
161 | 1. Access the application using the default credentials:
162 | - 用户名: `admin`
163 | - 密码: `password`
164 |
165 | 2. After logging in, go to the Settings page and change the default password.
166 |
167 |
168 | ### Managing API Keys
169 |
170 | 1. Navigate to the "API Keys" section from the dashboard.
171 |
172 | 2. Click "Add Key" to add a new API key.
173 |
174 | 3. Fill in the required information:
175 |
176 | - Name: Descriptive name for the key
177 | - Provider: Service provider (e.g., OpenAI, Anthropic)
178 | - Key Type: Simple API key or Composite key
179 | - API Key: Your actual API key Additional fields for Composite key (AppID, Secret Key)
180 | - Base URL: API endpoint URL (default values for known providers)
181 | - Billing URL: Link to the provider's billing page (recommended)
182 |
183 | 4. To test a key's connection, use the "Test" button on the dashboard.
184 |
185 | Note: Not only AI keys, but all related keys can be configured and stored.
186 |
187 |
188 | ## ⚠️ Security Recommendations
189 |
190 | 1. Change the default admin password immediately after first login.
191 | 2. Use a strong, unique `ENCRYPTION_KEY` for production deployments.
192 | 3. If self-hosting, ensure your server has HTTPS enabled.
193 | 4. Regularly backup your data.
194 | 5. Regularly rotate your API keys to minimize risk.
195 | 6. Store sensitive information such as API keys and encryption keys in environment variables.
196 |
197 |
198 | ## 🛠️ Configuration
199 |
200 | ### Environment Variables
201 |
202 | | Variable | Description | Default |
203 | | ---------------- | -------------------------------- | -------- |
204 | | `ENCRYPTION_KEY` | Key used for encrypting API keys | Required |
205 |
206 |
207 | ### Language Settings
208 |
209 | The application supports English and Chinese. Users can switch languages using the language selector in the interface.
210 |
211 | ## Contributing
212 |
213 | We welcome contributions from the community! To contribute, please follow these steps:
214 |
215 | 1. Fork the repository.
216 | 2. Create a new branch (`git checkout -b feature-branch`).
217 | 3. Make your changes and commit them (`git commit -m 'Add new feature'`).
218 | 4. Push to the branch (`git push origin feature-branch`).
219 | 5. Create a new Pull Request.
220 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # API密钥管理器
2 |
3 |
4 |
5 |
6 |
7 |
8 | English |
9 | 简体中文
10 |
11 |
12 | 一个安全且高效的API密钥管理系统,帮助开发者和团队轻松管理各种AI模型的API密钥。
13 | ## 前言
14 | API秘钥管理器旨在为AI开发者和企业提供一站式API密钥管理解决方案。在当今多样化的AI服务环境中,管理多个提供商的API密钥既繁琐又存在安全风险。本系统通过安全加密存储、状态监控和便捷管理功能,帮助用户高效组织和使用各类AI服务密钥,降低泄露风险,提升开发效率。系统采用本地存储技术,确保您的敏感密钥信息不会上传至云端,进一步增强数据安全性和隐私保护。无论您是个人开发者还是企业用户,API秘钥管理器都能满足您的API密钥管理需求,不仅限于AI密钥,其他相关密钥也可通过该管理器进行统一管理。
15 |
16 | 例如:[dify](https://github.com/langgenius/dify) 这个项目在使用时需要填写多个AI大模型的key,这是一个头疼的事,这也是这个项目的一个出发点
17 |
18 | ## ✨ 功能
19 |
20 | - [x] 🔑 **API密钥管理**: 安全地存储和管理来自多个AI服务提供商的API密钥
21 | - [x] 🔒 **安全加密**: 使用高级加密技术保护您的API密钥
22 | - [x] ⚡ **状态监控**: 实时监控API密钥的连接状态
23 | - [x] 🌐 **多语言支持**: 在中文和英文界面之间切换
24 | - [x] ⚙️ **自定义设置**: 根据您的需求自定义系统设置和偏好
25 | - [x] 🔄 **连接测试**: 直接从仪表盘测试API连接
26 | - [ ] 📊 **使用监控**: 监控API密钥的使用量和token消耗(待开发)
27 |
28 | ## 📋 目录
29 |
30 | - [功能](#-功能)
31 | - [演示](#-演示)
32 | - [安装](#-安装)
33 | - [Vercel部署](#vercel部署)
34 | - [本地安装](#本地安装)
35 | - [Docker部署](#docker部署)
36 | - [使用](#-使用指南)
37 | - [配置](#-配置选项)
38 |
39 | ## 📸 演示
40 |
41 | 访问我们的[演示站点](https://www.a888.online)体验应用程序。
42 |
43 | 密钥管理页面:密钥总览
44 |
45 |
46 |
47 | 添加密钥:基础密钥 + 复合密钥
48 |
49 |
50 |
51 | 仪表盘:监控连接情况
52 |
53 |
54 |
55 | 系统设置:密码更改等操作
56 |
57 |
58 |
59 |
60 | 默认登录凭据:
61 |
62 | - 用户名: `admin`
63 | - 密码: `password`
64 |
65 | 登录后若需长期使用建议更改密码并记住密码
66 |
67 | ## 📥 安装
68 |
69 | ### Vercel部署
70 |
71 | 使用Vercel部署API密钥管理器是最简单的方法:
72 |
73 | 1. 将此仓库 fork 到您的 GitHub 账户
74 | 2. 注册或登录 [Vercel](https://vercel.com)
75 | 3. 点击 "New Project" 并导入您的 forked 仓库
76 | 4. 配置以下环境变量:
77 | - `ENCRYPTION_KEY`: 用于加密API密钥的安全随机字符串
78 | - `ALLOW_REGISTRATION`: 设置为 "true" 或 "false" 以启用/禁用用户注册,注册功能已经弃用
79 | 5. 点击 "Deploy"
80 |
81 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fyourusername%2Fapi-key-manager)
82 |
83 | ### 本地安装
84 |
85 | #### 前提条件
86 |
87 | - Node.js 18.x 或更高版本
88 | - npm 或 yarn
89 | - Git
90 |
91 | #### 步骤
92 |
93 | 1. **克隆仓库**:
94 |
95 | ```bash
96 | git clone https://github.com/randomAndre/api-key-manager.git
97 | cd api-key-manager
98 | ```
99 | 2. **安装依赖**
100 |
101 | ```bash
102 | npm install
103 | # 或
104 | yarn install
105 | ```
106 |
107 |
108 | 3. **环境设置**
109 |
110 | 在根目录创建一个`.env.local`文件,包含以下变量:
111 |
112 | ```plaintext
113 | ENCRYPTION_KEY=your-secure-random-string
114 | ```
115 |
116 |
117 | 4. **开发模式**
118 |
119 | 要在开发模式下运行应用程序:
120 |
121 | ```bash
122 | npm run dev
123 | # 或
124 | yarn dev
125 | ```
126 |
127 | 应用程序将在`http://localhost:3000`上可用。
128 |
129 |
130 | 5. **生产构建**
131 |
132 | 要创建生产构建:
133 |
134 | ```bash
135 | npm run build
136 | npm start
137 | # 或
138 | yarn build
139 | yarn start
140 | ```
141 |
142 | ### **Docker部署**
143 |
144 | 您也可以使用Docker部署:
145 |
146 | ```bash
147 | # 构建Docker镜像
148 | docker build -t nanmeng-api-key-manager .
149 |
150 | # 运行容器
151 | docker run -p 3000:3000 -e ENCRYPTION_KEY=your-secure-key -e ALLOW_REGISTRATION=false nanmeng-api-key-manager
152 | ```
153 |
154 |
155 |
156 |
157 | ## 🚀 使用指南
158 |
159 | ### 首次设置
160 |
161 | 1. 使用默认凭据访问应用程序:
162 | - 用户名: `admin`
163 | - 密码: `password`
164 |
165 | 2. 登录后,转到设置页面并更改默认密码。
166 |
167 |
168 | ### 管理API密钥
169 |
170 | 1. 从仪表板导航到"API密钥"部分。
171 |
172 | 2. 点击"添加密钥"添加新的API密钥。
173 |
174 | 3. 填写所需信息:
175 |
176 | - 名称:密钥的描述性名称
177 | - 提供商:服务提供商(OpenAI、Anthropic等)
178 | - 密钥类型:简单API密钥或复合密钥
179 | - API密钥:您的实际API密钥/复合密钥的附加字段(AppID、Secret Key)
180 | - 基础URL:API端点URL(为已知提供商提供默认值)
181 | - 充值URL:提供商的账单页面链接(建议填写)
182 |
183 | 4. 要测试密钥的连接,请使用仪表板上的"测试"按钮。
184 |
185 | 注:不止是AI密钥,所有相关密钥皆可配置存储
186 |
187 |
188 | ### ⚠️ 安全建议
189 |
190 | 1. 首次登录后立即更改默认管理员密码。
191 | 2. 为生产部署使用强大且唯一的`ENCRYPTION_KEY`。
192 | 3. 如果自托管,确保您的服务器启用了HTTPS。
193 | 4. 定期备份您的数据。
194 | 5. 定期更换您的API密钥以降低风险。
195 | 6. 使用环境变量存储敏感信息,如API密钥和加密密钥。
196 |
197 | ## 🛠️ 配置选项
198 |
199 | ### 环境变量
200 |
201 | | 变量 | 描述 | 默认值 |
202 | | ---------------- | --------------------- | ------ |
203 | | `ENCRYPTION_KEY` | 用于加密API密钥的密钥 | 必填 |
204 |
205 | ## 语言设置
206 |
207 | 应用程序支持中文和英文。用户可以使用界面中的语言选择器切换语言。
208 |
209 | ## 贡献
210 |
211 | 我们欢迎社区的贡献!要贡献,请按照以下步骤操作:
212 |
213 | 1. Fork 仓库。
214 | 2. 创建一个新分支 (`git checkout -b feature-branch`)。
215 | 3. 进行更改并提交 (`git commit -m 'Add new feature'`)。
216 | 4. 推送到分支 (`git push origin feature-branch`)。
217 | 5. 创建一个新的 Pull Request。
218 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Currently, we are providing security updates for the following versions:
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 1.0.x | :white_check_mark: |
10 |
11 | ## Reporting a Vulnerability
12 |
13 | We take the security of API Key Manager seriously. If you believe you've found a security vulnerability, please follow these steps:
14 |
15 | 1. **Do not disclose the vulnerability publicly**
16 | 2. **Email us at [security@nanmengtech.com](mailto:security@nanmengtech.com)** with details about the vulnerability
17 | 3. Include steps to reproduce, if possible
18 | 4. Include potential impact of the vulnerability
19 |
20 | We will acknowledge receipt of your vulnerability report as soon as possible and will send you regular updates about our progress.
21 |
22 | ## What to Expect
23 |
24 | After you've submitted a vulnerability report:
25 |
26 | 1. We will acknowledge your report within 48 hours
27 | 2. We will provide an initial assessment of the report within 1 week
28 | 3. We will work on addressing the vulnerability and will keep you updated on our progress
29 | 4. Once the vulnerability is fixed, we will publicly acknowledge your responsible disclosure (unless you prefer to remain anonymous)
30 |
31 | Thank you for helping keep API Key Manager and its users safe!
32 |
33 |
--------------------------------------------------------------------------------
/app/dashboard/keys/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import ApiKeyList from "@/components/api-key-list"
3 | import { useLanguage } from "@/lib/i18n/language-context"
4 |
5 | export default function ApiKeysPage() {
6 | const { t } = useLanguage()
7 |
8 | return (
9 |
10 |
{t("apiKeys.title")}
11 |
12 |
13 | )
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import type React from "react"
4 | import DashboardLayout from "@/components/dashboard-layout"
5 | import { LanguageProvider } from "@/lib/i18n/language-context"
6 | import ErrorNotification from "@/components/error-notification"
7 |
8 | export default function DashboardRootLayout({
9 | children,
10 | }: {
11 | children: React.ReactNode
12 | }) {
13 | return (
14 |
15 |
16 | {children}
17 |
18 | )
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
4 | import ApiStatusCard from "@/components/api-balance-card"
5 | import { useLanguage } from "@/lib/i18n/language-context"
6 | import { useState, useEffect } from "react"
7 | import { apiKeyStorage } from "@/lib/storage"
8 | import { Progress } from "@/components/ui/progress"
9 | import { Activity } from "lucide-react"
10 |
11 | export default function DashboardPage() {
12 | const { t } = useLanguage()
13 | const [activeKeys, setActiveKeys] = useState(0)
14 | const [apiAvailability, setApiAvailability] = useState(0)
15 |
16 | useEffect(() => {
17 | const calculateApiStats = () => {
18 | // Get all API keys
19 | const keys = apiKeyStorage.getApiKeysByUserId(1)
20 |
21 | // Set the active keys count (total number of keys)
22 | setActiveKeys(keys.length)
23 |
24 | // Get connection test results from cache
25 | const cacheJson = localStorage.getItem("api_connection_test_results")
26 | if (!cacheJson) {
27 | setApiAvailability(0)
28 | return
29 | }
30 |
31 | const cache = JSON.parse(cacheJson)
32 |
33 | // Count available APIs (status 200-299)
34 | let availableCount = 0
35 | let totalTestedCount = 0
36 |
37 | keys.forEach((key) => {
38 | // Skip custom APIs for availability calculation
39 | if (key.provider === "Custom") return
40 |
41 | const testResult = cache[key.id]
42 | if (testResult) {
43 | totalTestedCount++
44 | if (testResult.status >= 200 && testResult.status < 300) {
45 | availableCount++
46 | }
47 | }
48 | })
49 |
50 | // Calculate availability percentage
51 | const availability = totalTestedCount > 0 ? Math.round((availableCount / totalTestedCount) * 100) : 0
52 |
53 | setApiAvailability(availability)
54 | }
55 |
56 | calculateApiStats()
57 | }, [])
58 |
59 | // Add an event listener to update stats when API statuses are refreshed
60 | useEffect(() => {
61 | const handleApiStatusUpdate = () => {
62 | // Recalculate API stats when statuses are updated
63 | const calculateApiStats = () => {
64 | // Get all API keys
65 | const keys = apiKeyStorage.getApiKeysByUserId(1)
66 |
67 | // Set the active keys count (total number of keys)
68 | setActiveKeys(keys.length)
69 |
70 | // Get connection test results from cache
71 | const cacheJson = localStorage.getItem("api_connection_test_results")
72 | if (!cacheJson) {
73 | setApiAvailability(0)
74 | return
75 | }
76 |
77 | const cache = JSON.parse(cacheJson)
78 |
79 | // Count available APIs (status 200-299)
80 | let availableCount = 0
81 | let totalTestedCount = 0
82 |
83 | keys.forEach((key) => {
84 | // Skip custom APIs for availability calculation
85 | if (key.provider === "Custom") return
86 |
87 | const testResult = cache[key.id]
88 | if (testResult) {
89 | totalTestedCount++
90 | if (testResult.status >= 200 && testResult.status < 300) {
91 | availableCount++
92 | }
93 | }
94 | })
95 |
96 | // Calculate availability percentage
97 | const availability = totalTestedCount > 0 ? Math.round((availableCount / totalTestedCount) * 100) : 0
98 |
99 | setApiAvailability(availability)
100 | }
101 |
102 | calculateApiStats()
103 | }
104 |
105 | // Add event listener for API status updates
106 | window.addEventListener("api-status-updated", handleApiStatusUpdate)
107 |
108 | // Clean up event listener
109 | return () => {
110 | window.removeEventListener("api-status-updated", handleApiStatusUpdate)
111 | }
112 | }, [])
113 |
114 | return (
115 |
116 |
{t("dashboard.title")}
117 |
118 |
119 |
120 |
121 | {t("dashboard.activeKeys")}
122 |
123 |
124 | {activeKeys}
125 | {t("dashboard.comparedToLastMonth")}
126 |
127 |
128 |
129 |
130 | {t("dashboard.encryptedKeys")}
131 |
132 |
133 | 100%
134 | {t("dashboard.allKeysEncrypted")}
135 |
136 |
137 |
138 |
139 | {t("dashboard.apiAvailability")}
140 |
141 |
142 |
143 | {apiAvailability}%
144 |
147 |
148 | {apiAvailability >= 90
149 | ? t("dashboard.apiAvailabilityNormal")
150 | : apiAvailability >= 70
151 | ? t("dashboard.apiAvailabilityDelayed")
152 | : t("dashboard.apiAvailabilityIssues")}
153 |
154 |
155 |
156 |
157 |
158 | {/* API状态卡片 */}
159 |
162 |
163 | )
164 | }
165 |
166 |
--------------------------------------------------------------------------------
/app/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import SettingsForm from "@/components/settings-form"
3 | import { useLanguage } from "@/lib/i18n/language-context"
4 |
5 | export default function SettingsPage() {
6 | const { t } = useLanguage()
7 |
8 | return (
9 |
10 |
{t("settings.title")}
11 |
12 |
13 | )
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 221.2 83.2% 53.3%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 221.2 83.2% 53.3%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 217.2 91.2% 59.8%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 224.3 76.3% 48%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react"
2 | import type { Metadata } from "next"
3 | import { Inter } from "next/font/google"
4 | import "./globals.css"
5 | import { ThemeProvider } from "@/components/theme-provider"
6 |
7 | const inter = Inter({ subsets: ["latin"] })
8 |
9 | export const metadata: Metadata = {
10 | title: "南梦API秘钥管理",
11 | description: "安全管理各种 AI 模型的 API 密钥",
12 | icons: {
13 | icon: [
14 | {
15 | url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/75cda7542df94f9e8534d82f9e31cae3_3-1flDTgHIuogz0ioIUPjC3BE9xbsIdP.png",
16 | sizes: "32x32",
17 | type: "image/png",
18 | },
19 | {
20 | url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/75cda7542df94f9e8534d82f9e31cae3_3-1flDTgHIuogz0ioIUPjC3BE9xbsIdP.png",
21 | sizes: "16x16",
22 | type: "image/png",
23 | },
24 | ],
25 | apple: [
26 | {
27 | url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/75cda7542df94f9e8534d82f9e31cae3_3-1flDTgHIuogz0ioIUPjC3BE9xbsIdP.png",
28 | sizes: "180x180",
29 | type: "image/png",
30 | },
31 | ],
32 | },
33 | generator: 'v0.dev'
34 | }
35 |
36 | export default function RootLayout({
37 | children,
38 | }: Readonly<{
39 | children: React.ReactNode
40 | }>) {
41 | return (
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 |
49 | )
50 | }
51 |
52 |
53 |
54 | import './globals.css'
--------------------------------------------------------------------------------
/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import LoginForm from "@/components/login-form"
4 | import { useLanguage } from "@/lib/i18n/language-context"
5 | import LanguageSwitcher from "@/components/language-switcher"
6 | import { LanguageProvider } from "@/lib/i18n/language-context"
7 |
8 | export default function LoginPage() {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | function LoginContent() {
17 | const { t } = useLanguage()
18 |
19 | return (
20 |
21 | {/* 添加语言切换器到右上角 */}
22 |
23 |
24 |
25 |
26 |
27 |
{t("app.title")}
28 |
29 |
30 |
31 | )
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { Card, CardContent } from "@/components/ui/card"
5 | import { useLanguage } from "@/lib/i18n/language-context"
6 | import LanguageSwitcher from "@/components/language-switcher"
7 | import { LanguageProvider } from "@/lib/i18n/language-context"
8 | import Link from "next/link"
9 | import { Mail, Key, Shield, BarChart, Settings, Github } from "lucide-react"
10 |
11 | export default function Home() {
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | function HomeContent() {
20 | const { t } = useLanguage()
21 |
22 | return (
23 |
24 | {/* 导航栏 */}
25 |
26 |
27 |
28 |
29 | {t("app.title")}
30 |
31 |
32 |
33 |
34 | {t("common.login")}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {/* 英雄区域 */}
42 |
43 |
44 | {t("app.title")}
45 |
46 |
{t("home.hero.subtitle")}
47 |
48 |
49 |
50 | {t("home.hero.cta")}
51 |
52 |
53 |
54 |
55 |
56 | {/* 功能特点 */}
57 |
58 |
59 |
60 |
61 | {t("home.features.title")}
62 |
63 |
64 | {t("home.features.subtitle")}
65 |
66 |
67 |
68 |
69 | }
71 | title={t("home.features.keyManagement.title")}
72 | description={t("home.features.keyManagement.description")}
73 | />
74 | }
76 | title={t("home.features.security.title")}
77 | description={t("home.features.security.description")}
78 | />
79 | }
81 | title={t("home.features.monitoring.title")}
82 | description={t("home.features.monitoring.description")}
83 | />
84 | }
86 | title={t("home.features.settings.title")}
87 | description={t("home.features.settings.description")}
88 | />
89 |
90 |
91 |
92 |
93 | {/* 联系方式 */}
94 |
95 |
96 |
{t("home.contact.title")}
97 |
{t("home.contact.subtitle")}
98 |
99 |
100 | nanmeng@nanmengtech.com
101 |
102 |
103 |
104 |
105 | {/* 页脚 */}
106 |
121 |
122 | )
123 | }
124 |
125 | function FeatureCard({ icon, title, description }) {
126 | return (
127 |
128 |
129 | {icon}
130 | {title}
131 | {description}
132 |
133 |
134 | )
135 | }
136 |
137 |
--------------------------------------------------------------------------------
/app/register/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import type React from "react"
4 |
5 | import { LanguageProvider } from "@/lib/i18n/language-context"
6 |
7 | export default function RegisterLayout({
8 | children,
9 | }: {
10 | children: React.ReactNode
11 | }) {
12 | return {children}
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/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 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/dashboard-layout.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import type React from "react"
4 |
5 | import { useState } from "react"
6 | import Link from "next/link"
7 | import { usePathname } from "next/navigation"
8 | import { Button } from "@/components/ui/button"
9 | import { LayoutDashboard, Key, Settings, LogOut, Menu, X } from "lucide-react"
10 | import LanguageSwitcher from "@/components/language-switcher"
11 | import { useLanguage } from "@/lib/i18n/language-context"
12 |
13 | interface DashboardLayoutProps {
14 | children: React.ReactNode
15 | }
16 |
17 | export default function DashboardLayout({ children }: DashboardLayoutProps) {
18 | const pathname = usePathname()
19 | const [sidebarOpen, setSidebarOpen] = useState(false)
20 | const { t } = useLanguage()
21 |
22 | const toggleSidebar = () => {
23 | setSidebarOpen(!sidebarOpen)
24 | }
25 |
26 | const navItems = [
27 | { href: "/dashboard", label: t("common.dashboard"), icon: },
28 | { href: "/dashboard/keys", label: t("common.apiKeys"), icon: },
29 | { href: "/dashboard/settings", label: t("common.settings"), icon: },
30 | ]
31 |
32 | return (
33 |
34 | {/* 移动端菜单按钮 */}
35 |
36 |
37 | {sidebarOpen ? : }
38 |
39 |
40 |
41 | {/* 语言切换器 - 移动端 */}
42 |
43 |
44 |
45 |
46 | {/* 侧边栏 - 减小宽度和间距 */}
47 |
53 |
54 |
55 |
{t("app.title")}
56 |
57 |
58 |
59 |
60 |
61 | {navItems.map((item) => (
62 |
70 | {item.icon}
71 | {item.label}
72 |
73 | ))}
74 |
75 |
76 |
77 |
78 |
79 | {t("common.logout")}
80 |
81 |
82 |
83 |
84 |
85 |
86 | {/* 主内容区 - 调整左边距并居中内容 */}
87 |
88 | {children}
89 |
90 |
91 | )
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/components/error-notification.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect, useState } from "react"
4 | import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
5 | import { X } from "lucide-react"
6 | import { Button } from "@/components/ui/button"
7 |
8 | interface ErrorEvent extends CustomEvent {
9 | detail: {
10 | title: string
11 | message: string
12 | }
13 | }
14 |
15 | export default function ErrorNotification() {
16 | const [error, setError] = useState<{ title: string; message: string } | null>(null)
17 | const [visible, setVisible] = useState(false)
18 |
19 | useEffect(() => {
20 | const handleError = (event: Event) => {
21 | const errorEvent = event as ErrorEvent
22 | setError(errorEvent.detail)
23 | setVisible(true)
24 |
25 | // 自动5秒后隐藏
26 | setTimeout(() => {
27 | setVisible(false)
28 | }, 5000)
29 | }
30 |
31 | window.addEventListener("app-error", handleError)
32 |
33 | return () => {
34 | window.removeEventListener("app-error", handleError)
35 | }
36 | }, [])
37 |
38 | if (!error || !visible) return null
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
{error.title}
46 |
{error.message}
47 |
48 |
setVisible(false)}
53 | >
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/components/language-switcher.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { Button } from "@/components/ui/button"
5 | import { Check, Globe } from "lucide-react"
6 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
7 | import { useLanguage } from "@/lib/i18n/language-context"
8 | import { useToast } from "@/hooks/use-toast"
9 |
10 | export default function LanguageSwitcher() {
11 | const { language, setLanguage, t } = useLanguage()
12 | const { toast } = useToast()
13 | const [isOpen, setIsOpen] = useState(false)
14 |
15 | const languages = [
16 | { code: "zh-CN", name: "中文简体" },
17 | { code: "en-US", name: "English" },
18 | ]
19 |
20 | const handleLanguageChange = (langCode: "zh-CN" | "en-US") => {
21 | setLanguage(langCode)
22 | setIsOpen(false)
23 |
24 | toast({
25 | title: t("settings.languageChanged"),
26 | description: langCode === "zh-CN" ? "已切换到中文" : "Switched to English",
27 | })
28 | }
29 |
30 | return (
31 |
32 |
33 |
34 |
35 | {t("settings.selectLanguage")}
36 |
37 |
38 |
39 | {languages.map((lang) => (
40 | handleLanguageChange(lang.code as "zh-CN" | "en-US")}
43 | className="flex items-center justify-between"
44 | >
45 | {lang.name}
46 | {language === lang.code && }
47 |
48 | ))}
49 |
50 |
51 | )
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/components/login-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import type React from "react"
4 |
5 | import { useState, useEffect } from "react"
6 | import { useRouter } from "next/navigation"
7 | import { Button } from "@/components/ui/button"
8 | import { Input } from "@/components/ui/input"
9 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
10 | import { Label } from "@/components/ui/label"
11 | import { useLanguage } from "@/lib/i18n/language-context"
12 | import { userStorage, initStorage } from "@/lib/storage"
13 |
14 | export default function LoginForm() {
15 | const { t } = useLanguage()
16 | const [username, setUsername] = useState("")
17 | const [password, setPassword] = useState("")
18 | const [loading, setLoading] = useState(false)
19 | const [error, setError] = useState("")
20 | const router = useRouter()
21 |
22 | // 初始化本地存储
23 | useEffect(() => {
24 | initStorage()
25 | }, [])
26 |
27 | // 登录功能
28 | const handleLogin = async (e: React.FormEvent) => {
29 | e.preventDefault()
30 | setLoading(true)
31 | setError("")
32 |
33 | try {
34 | // 模拟延迟
35 | await new Promise((resolve) => setTimeout(resolve, 800))
36 |
37 | // 验证用户
38 | const user = userStorage.getUserByUsername(username)
39 |
40 | if (user && user.password === password) {
41 | // 存储会话信息
42 | localStorage.setItem(
43 | "user",
44 | JSON.stringify({
45 | id: user.id,
46 | username: user.username,
47 | email: user.email,
48 | }),
49 | )
50 |
51 | router.push("/dashboard")
52 | } else {
53 | setError(t("login.error"))
54 | }
55 | } catch (err) {
56 | setError(t("error.loginFailed"))
57 | } finally {
58 | setLoading(false)
59 | }
60 | }
61 |
62 | return (
63 |
64 |
65 | {t("login.title")}
66 | {t("login.description")}
67 |
68 |
69 |
94 |
95 |
96 |
97 | {loading ? t("common.loading") : t("login.button")}
98 |
99 |
102 |
103 |
104 | )
105 | }
106 |
107 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { ThemeProvider as NextThemesProvider } from "next-themes"
3 | import type { ThemeProviderProps } from "next-themes"
4 |
5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
6 | return {children}
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/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: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13 | },
14 | },
15 | defaultVariants: {
16 | variant: "default",
17 | },
18 | },
19 | )
20 |
21 | const Alert = React.forwardRef<
22 | HTMLDivElement,
23 | React.HTMLAttributes & VariantProps
24 | >(({ className, variant, ...props }, ref) => (
25 |
26 | ))
27 | Alert.displayName = "Alert"
28 |
29 | const AlertTitle = React.forwardRef>(
30 | ({ className, ...props }, ref) => (
31 |
32 | ),
33 | )
34 | AlertTitle.displayName = "AlertTitle"
35 |
36 | const AlertDescription = React.forwardRef>(
37 | ({ className, ...props }, ref) => (
38 |
39 | ),
40 | )
41 | AlertDescription.displayName = "AlertDescription"
42 |
43 | export { Alert, AlertTitle, AlertDescription }
44 |
45 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeft, ChevronRight } from "lucide-react"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
58 | IconRight: ({ ...props }) => ,
59 | }}
60 | {...props}
61 | />
62 | )
63 | }
64 | Calendar.displayName = "Calendar"
65 |
66 | export { Calendar }
67 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react"
7 | import { ArrowLeft, ArrowRight } from "lucide-react"
8 |
9 | import { cn } from "@/lib/utils"
10 | import { Button } from "@/components/ui/button"
11 |
12 | type CarouselApi = UseEmblaCarouselType[1]
13 | type UseCarouselParameters = Parameters
14 | type CarouselOptions = UseCarouselParameters[0]
15 | type CarouselPlugin = UseCarouselParameters[1]
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions
19 | plugins?: CarouselPlugin
20 | orientation?: "horizontal" | "vertical"
21 | setApi?: (api: CarouselApi) => void
22 | }
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0]
26 | api: ReturnType[1]
27 | scrollPrev: () => void
28 | scrollNext: () => void
29 | canScrollPrev: boolean
30 | canScrollNext: boolean
31 | } & CarouselProps
32 |
33 | const CarouselContext = React.createContext(null)
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext)
37 |
38 | if (!context) {
39 | throw new Error("useCarousel must be used within a ")
40 | }
41 |
42 | return context
43 | }
44 |
45 | const Carousel = React.forwardRef<
46 | HTMLDivElement,
47 | React.HTMLAttributes & CarouselProps
48 | >(
49 | (
50 | {
51 | orientation = "horizontal",
52 | opts,
53 | setApi,
54 | plugins,
55 | className,
56 | children,
57 | ...props
58 | },
59 | ref
60 | ) => {
61 | const [carouselRef, api] = useEmblaCarousel(
62 | {
63 | ...opts,
64 | axis: orientation === "horizontal" ? "x" : "y",
65 | },
66 | plugins
67 | )
68 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
69 | const [canScrollNext, setCanScrollNext] = React.useState(false)
70 |
71 | const onSelect = React.useCallback((api: CarouselApi) => {
72 | if (!api) {
73 | return
74 | }
75 |
76 | setCanScrollPrev(api.canScrollPrev())
77 | setCanScrollNext(api.canScrollNext())
78 | }, [])
79 |
80 | const scrollPrev = React.useCallback(() => {
81 | api?.scrollPrev()
82 | }, [api])
83 |
84 | const scrollNext = React.useCallback(() => {
85 | api?.scrollNext()
86 | }, [api])
87 |
88 | const handleKeyDown = React.useCallback(
89 | (event: React.KeyboardEvent) => {
90 | if (event.key === "ArrowLeft") {
91 | event.preventDefault()
92 | scrollPrev()
93 | } else if (event.key === "ArrowRight") {
94 | event.preventDefault()
95 | scrollNext()
96 | }
97 | },
98 | [scrollPrev, scrollNext]
99 | )
100 |
101 | React.useEffect(() => {
102 | if (!api || !setApi) {
103 | return
104 | }
105 |
106 | setApi(api)
107 | }, [api, setApi])
108 |
109 | React.useEffect(() => {
110 | if (!api) {
111 | return
112 | }
113 |
114 | onSelect(api)
115 | api.on("reInit", onSelect)
116 | api.on("select", onSelect)
117 |
118 | return () => {
119 | api?.off("select", onSelect)
120 | }
121 | }, [api, onSelect])
122 |
123 | return (
124 |
137 |
145 | {children}
146 |
147 |
148 | )
149 | }
150 | )
151 | Carousel.displayName = "Carousel"
152 |
153 | const CarouselContent = React.forwardRef<
154 | HTMLDivElement,
155 | React.HTMLAttributes
156 | >(({ className, ...props }, ref) => {
157 | const { carouselRef, orientation } = useCarousel()
158 |
159 | return (
160 |
171 | )
172 | })
173 | CarouselContent.displayName = "CarouselContent"
174 |
175 | const CarouselItem = React.forwardRef<
176 | HTMLDivElement,
177 | React.HTMLAttributes
178 | >(({ className, ...props }, ref) => {
179 | const { orientation } = useCarousel()
180 |
181 | return (
182 |
193 | )
194 | })
195 | CarouselItem.displayName = "CarouselItem"
196 |
197 | const CarouselPrevious = React.forwardRef<
198 | HTMLButtonElement,
199 | React.ComponentProps
200 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
202 |
203 | return (
204 |
219 |
220 | Previous slide
221 |
222 | )
223 | })
224 | CarouselPrevious.displayName = "CarouselPrevious"
225 |
226 | const CarouselNext = React.forwardRef<
227 | HTMLButtonElement,
228 | React.ComponentProps
229 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
230 | const { orientation, scrollNext, canScrollNext } = useCarousel()
231 |
232 | return (
233 |
248 |
249 | Next slide
250 |
251 | )
252 | })
253 | CarouselNext.displayName = "CarouselNext"
254 |
255 | export {
256 | type CarouselApi,
257 | Carousel,
258 | CarouselContent,
259 | CarouselItem,
260 | CarouselPrevious,
261 | CarouselNext,
262 | }
263 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
5 |
6 | const Collapsible = CollapsiblePrimitive.Root
7 |
8 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
9 |
10 | const CollapsibleContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
19 | ))
20 | CollapsibleContent.displayName = "CollapsibleContent"
21 |
22 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
23 |
24 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | const CommandDialog = ({ children, ...props }: DialogProps) => {
27 | return (
28 |
29 |
30 |
31 | {children}
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = "Drawer"
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = "DrawerContent"
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = "DrawerHeader"
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = "DrawerFooter"
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { OTPInput, OTPInputContext } from "input-otp"
5 | import { Dot } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const InputOTP = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, containerClassName, ...props }, ref) => (
13 |
22 | ))
23 | InputOTP.displayName = "InputOTP"
24 |
25 | const InputOTPGroup = React.forwardRef<
26 | React.ElementRef<"div">,
27 | React.ComponentPropsWithoutRef<"div">
28 | >(({ className, ...props }, ref) => (
29 |
30 | ))
31 | InputOTPGroup.displayName = "InputOTPGroup"
32 |
33 | const InputOTPSlot = React.forwardRef<
34 | React.ElementRef<"div">,
35 | React.ComponentPropsWithoutRef<"div"> & { index: number }
36 | >(({ index, className, ...props }, ref) => {
37 | const inputOTPContext = React.useContext(OTPInputContext)
38 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
39 |
40 | return (
41 |
50 | {char}
51 | {hasFakeCaret && (
52 |
55 | )}
56 |
57 | )
58 | })
59 | InputOTPSlot.displayName = "InputOTPSlot"
60 |
61 | const InputOTPSeparator = React.forwardRef<
62 | React.ElementRef<"div">,
63 | React.ComponentPropsWithoutRef<"div">
64 | >(({ ...props }, ref) => (
65 |
66 |
67 |
68 | ))
69 | InputOTPSeparator.displayName = "InputOTPSeparator"
70 |
71 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
72 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { ButtonProps, buttonVariants } from "@/components/ui/button"
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8 |
14 | )
15 | Pagination.displayName = "Pagination"
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<"ul">
20 | >(({ className, ...props }, ref) => (
21 |
26 | ))
27 | PaginationContent.displayName = "PaginationContent"
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<"li">
32 | >(({ className, ...props }, ref) => (
33 |
34 | ))
35 | PaginationItem.displayName = "PaginationItem"
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean
39 | } & Pick &
40 | React.ComponentProps<"a">
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = "icon",
46 | ...props
47 | }: PaginationLinkProps) => (
48 |
59 | )
60 | PaginationLink.displayName = "PaginationLink"
61 |
62 | const PaginationPrevious = ({
63 | className,
64 | ...props
65 | }: React.ComponentProps) => (
66 |
72 |
73 | Previous
74 |
75 | )
76 | PaginationPrevious.displayName = "PaginationPrevious"
77 |
78 | const PaginationNext = ({
79 | className,
80 | ...props
81 | }: React.ComponentProps) => (
82 |
88 | Next
89 |
90 |
91 | )
92 | PaginationNext.displayName = "PaginationNext"
93 |
94 | const PaginationEllipsis = ({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"span">) => (
98 |
103 |
104 | More pages
105 |
106 | )
107 | PaginationEllipsis.displayName = "PaginationEllipsis"
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
17 |
21 |
22 | ))
23 | Progress.displayName = ProgressPrimitive.Root.displayName
24 |
25 | export { Progress }
26 |
27 |
--------------------------------------------------------------------------------
/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 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const RadioGroup = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => {
12 | return
13 | })
14 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
15 |
16 | const RadioGroupItem = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => {
20 | return (
21 |
29 |
30 |
31 |
32 |
33 | )
34 | })
35 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
36 |
37 | export { RadioGroup, RadioGroupItem }
38 |
39 |
--------------------------------------------------------------------------------
/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { GripVertical } from "lucide-react"
4 | import * as ResizablePrimitive from "react-resizable-panels"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | )
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | )
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
46 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ))
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext)
41 |
42 | return (
43 |
54 | {children}
55 |
56 | )
57 | })
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3 min-w-10",
20 | sm: "h-9 px-2.5 min-w-9",
21 | lg: "h-11 px-5 min-w-11",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/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 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/lib/actions/auth-actions.ts:
--------------------------------------------------------------------------------
1 | import { userStorage } from "@/lib/storage"
2 | import { cookies, revalidatePath } from "next/headers"
3 | import { userRepository } from "@/lib/repositories/user.repository"
4 | import { activityLogRepository } from "@/lib/repositories/activity-log.repository"
5 |
6 | interface UserSession {
7 | id: string
8 | username: string
9 | email: string
10 | }
11 |
12 | export async function loginUser(formData: FormData): Promise<{ success: boolean; message?: string }> {
13 | const username = formData.get("username") as string
14 | const password = formData.get("password") as string
15 |
16 | // 验证输入
17 | if (!username || !password) {
18 | return { success: false, message: "用户名和密码都是必填的" }
19 | }
20 |
21 | try {
22 | // 尝试使用数据库验证
23 | try {
24 | // 验证用户凭证
25 | const user = await userRepository.verifyCredentials(username, password)
26 |
27 | if (user) {
28 | // 更新最后登录时间
29 | await userRepository.updateLastLogin(user.id)
30 |
31 | // 记录登录活动
32 | try {
33 | await activityLogRepository.logLogin(user.id)
34 | } catch (error) {
35 | console.error("记录活动日志失败:", error)
36 | // 继续执行,不中断登录流程
37 | }
38 |
39 | // 创建用户会话
40 | const session: UserSession = {
41 | id: String(user.id),
42 | username: user.username,
43 | email: user.email,
44 | }
45 |
46 | // 将会话存储在安全的HTTP-only cookie中
47 | cookies().set("user_session", JSON.stringify(session), {
48 | httpOnly: true,
49 | secure: process.env.NODE_ENV === "production",
50 | maxAge: 60 * 60 * 24 * 7, // 7天
51 | path: "/",
52 | })
53 |
54 | revalidatePath("/", "layout")
55 |
56 | return { success: true }
57 | }
58 | } catch (dbError) {
59 | console.error("数据库验证失败,尝试使用本地存储:", dbError)
60 | // 继续执行,尝试本地存储
61 | }
62 |
63 | // 如果数据库验证失败,尝试使用本地存储
64 | console.log("尝试使用本地存储验证用户")
65 | const localUser = userStorage.getUserByUsername(username)
66 |
67 | if (!localUser || localUser.password !== password) {
68 | return { success: false, message: "用户名或密码错误" }
69 | }
70 |
71 | // 创建用户会话
72 | const session: UserSession = {
73 | id: String(localUser.id),
74 | username: localUser.username,
75 | email: localUser.email,
76 | }
77 |
78 | // 将会话存储在安全的HTTP-only cookie中
79 | cookies().set("user_session", JSON.stringify(session), {
80 | httpOnly: true,
81 | secure: process.env.NODE_ENV === "production",
82 | maxAge: 60 * 60 * 24 * 7, // 7天
83 | path: "/",
84 | })
85 |
86 | revalidatePath("/", "layout")
87 |
88 | return { success: true }
89 | } catch (error) {
90 | console.error("登录失败:", error)
91 | return { success: false, message: "登录失败,请稍后重试" }
92 | }
93 | }
94 |
95 | export async function getCurrentUser(): Promise<{ id: number; username: string; email: string } | null> {
96 | const session = cookies().get("user_session")?.value
97 |
98 | if (!session) {
99 | return null
100 | }
101 |
102 | try {
103 | const userSession = JSON.parse(session)
104 | return {
105 | id: Number(userSession.id),
106 | username: userSession.username,
107 | email: userSession.email,
108 | }
109 | } catch (error) {
110 | console.error("获取当前用户会话失败:", error)
111 | return null
112 | }
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/lib/api-connection.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * API连接测试工具
3 | * 用于测试不同提供商的API连接状态
4 | */
5 |
6 | import type { ApiKey } from "@/lib/storage"
7 |
8 | // 连接测试结果类型
9 | export interface ConnectionTestResult {
10 | status: number
11 | message: string
12 | testedAt: string
13 | latency: number // 延迟字段,单位ms
14 | }
15 |
16 | // 缓存键名
17 | const CONNECTION_TEST_CACHE = "api_connection_test_results"
18 |
19 | // 缓存过期时间(毫秒)- 1天
20 | const CACHE_EXPIRY = 24 * 60 * 60 * 1000
21 |
22 | /**
23 | * 获取认证头部
24 | */
25 | function getAuthHeaders(apiKey: ApiKey): Record {
26 | switch (apiKey.provider) {
27 | case "OpenAI":
28 | return { Authorization: `Bearer ${apiKey.key}` }
29 | case "Anthropic":
30 | return { "x-api-key": apiKey.key }
31 | case "Baidu":
32 | // 百度需要特殊处理,这里简化处理
33 | return { Authorization: `Bearer ${apiKey.key}` }
34 | case "Google":
35 | return { Authorization: `Bearer ${apiKey.key}` }
36 | case "Meta":
37 | return { Authorization: `Bearer ${apiKey.key}` }
38 | case "Mistral":
39 | return { Authorization: `Bearer ${apiKey.key}` }
40 | case "Cohere":
41 | return { Authorization: `Bearer ${apiKey.key}` }
42 | default:
43 | return { Authorization: `Bearer ${apiKey.key}` }
44 | }
45 | }
46 |
47 | /**
48 | * 处理响应状态
49 | */
50 | function processResponse(status: number): ConnectionTestResult {
51 | let message = ""
52 |
53 | if (status >= 200 && status < 300) {
54 | message = "连接正常"
55 | } else if (status === 401 || status === 403) {
56 | message = "认证失败"
57 | } else if (status === 429) {
58 | message = "请求频率限制"
59 | } else {
60 | message = `连接异常 (${status})`
61 | }
62 |
63 | return {
64 | status,
65 | message,
66 | testedAt: new Date().toISOString(),
67 | latency: 0, // 默认值,会被实际测量值覆盖
68 | }
69 | }
70 |
71 | /**
72 | * 测试API连接
73 | */
74 | export async function testApiConnection(apiKey: ApiKey): Promise {
75 | try {
76 | // 直接使用用户提供的baseUrl
77 | const url = apiKey.baseUrl
78 |
79 | // 如果URL为空,返回错误
80 | if (!url || url.trim() === "") {
81 | return {
82 | status: 0,
83 | message: "URL不能为空",
84 | testedAt: new Date().toISOString(),
85 | latency: 0,
86 | }
87 | }
88 |
89 | const headers = getAuthHeaders(apiKey)
90 |
91 | // 记录开始时间
92 | const startTime = performance.now()
93 |
94 | const response = await fetch(url, {
95 | method: "GET",
96 | headers,
97 | // 设置较短的超时时间,避免长时间等待
98 | signal: AbortSignal.timeout(10000),
99 | })
100 |
101 | // 计算延迟时间(毫秒)
102 | const latency = Math.round(performance.now() - startTime)
103 |
104 | // 将延迟添加到结果中
105 | return {
106 | ...processResponse(response.status),
107 | latency,
108 | }
109 | } catch (error) {
110 | console.error(`测试API连接失败:`, error)
111 |
112 | // 处理不同类型的错误
113 | let errorMessage = "未知错误"
114 | if (error instanceof TypeError && error.message.includes("NetworkError")) {
115 | errorMessage = "网络错误"
116 | } else if (error instanceof TypeError && error.message.includes("Failed to fetch")) {
117 | errorMessage = "无法连接到服务器"
118 | } else if (error instanceof DOMException && error.name === "AbortError") {
119 | errorMessage = "连接超时"
120 | } else if (error instanceof TypeError && error.message.includes("Invalid URL")) {
121 | errorMessage = "无效的URL格式"
122 | }
123 |
124 | return {
125 | status: 0,
126 | message: errorMessage,
127 | testedAt: new Date().toISOString(),
128 | latency: 0, // 连接失败时延迟为0
129 | }
130 | }
131 | }
132 |
133 | /**
134 | * 从缓存获取测试结果
135 | */
136 | export function getCachedTestResult(apiKeyId: number): ConnectionTestResult | null {
137 | if (typeof window === "undefined") return null
138 |
139 | try {
140 | const cacheJson = localStorage.getItem(CONNECTION_TEST_CACHE)
141 | if (!cacheJson) return null
142 |
143 | const cache = JSON.parse(cacheJson)
144 | const result = cache[apiKeyId]
145 |
146 | if (!result) return null
147 |
148 | // 检查缓存是否过期
149 | const testedAt = new Date(result.testedAt).getTime()
150 | const now = new Date().getTime()
151 | if (now - testedAt > CACHE_EXPIRY) {
152 | return null // 缓存已过期
153 | }
154 |
155 | return result
156 | } catch (error) {
157 | console.error("获取缓存测试结果失败:", error)
158 | return null
159 | }
160 | }
161 |
162 | /**
163 | * 缓存测试结果
164 | */
165 | export function cacheTestResult(apiKeyId: number, result: ConnectionTestResult): void {
166 | if (typeof window === "undefined") return
167 |
168 | try {
169 | const cacheJson = localStorage.getItem(CONNECTION_TEST_CACHE)
170 | const cache = cacheJson ? JSON.parse(cacheJson) : {}
171 |
172 | cache[apiKeyId] = result
173 | localStorage.setItem(CONNECTION_TEST_CACHE, JSON.stringify(cache))
174 | } catch (error) {
175 | console.error("缓存测试结果失败:", error)
176 | }
177 | }
178 |
179 | /**
180 | * 测试API连接并缓存结果
181 | */
182 | export async function testAndCacheConnection(apiKey: ApiKey): Promise {
183 | const result = await testApiConnection(apiKey)
184 | cacheTestResult(apiKey.id, result)
185 | return result
186 | }
187 |
188 | /**
189 | * 获取充值URL
190 | */
191 | export function getRechargeUrl(provider: string): string {
192 | switch (provider) {
193 | case "OpenAI":
194 | return "https://platform.openai.com/account/billing/overview"
195 | case "Anthropic":
196 | return "https://console.anthropic.com/account/billing"
197 | case "Baidu":
198 | return "https://console.bce.baidu.com/billing/#/billing/cbm/recharge"
199 | case "Google":
200 | return "https://console.cloud.google.com/billing"
201 | case "Meta":
202 | return "https://llama-api.meta.com/billing"
203 | case "Mistral":
204 | return "https://console.mistral.ai/billing/"
205 | case "Cohere":
206 | return "https://dashboard.cohere.com/account/billing"
207 | default:
208 | return "#"
209 | }
210 | }
211 |
212 |
--------------------------------------------------------------------------------
/lib/api-key-storage.ts:
--------------------------------------------------------------------------------
1 | // lib/api-key-storage.ts
2 | /**
3 | * 本地存储服务 - 使用localStorage替代数据库
4 | * 注意:这种方式只适合小型应用和演示项目
5 | */
6 |
7 | import { decrypt } from "@/lib/encryption"
8 |
9 | // 用户类型
10 | export interface User {
11 | id: number
12 | username: string
13 | password: string
14 | email: string
15 | created_at: string
16 | }
17 |
18 | // API密钥类型
19 | export interface ApiKey {
20 | id: number
21 | userId: number
22 | name: string
23 | key: string
24 | type: "apikey" | "complex"
25 | provider: string
26 | model?: string
27 | appId?: string
28 | secretKey?: string
29 | baseUrl: string
30 | createdAt: string
31 | lastUsed: string
32 | }
33 |
34 | export const apiKeyStorage = {
35 | // 获取用户的所有 API 密钥
36 | getApiKeysByUserId(userId: number): ApiKey[] {
37 | if (typeof window === "undefined") return []
38 | const apiKeys = localStorage.getItem("api_key_manager_api_keys")
39 | const keys = apiKeys ? JSON.parse(apiKeys) : []
40 | const userKeys = keys.filter((key: ApiKey) => key.userId === userId)
41 |
42 | // 返回解密后的密钥
43 | return userKeys.map((key: ApiKey) => ({
44 | ...key,
45 | key: key.key ? decrypt(key.key) : "",
46 | appId: key.appId ? decrypt(key.appId) : undefined,
47 | secretKey: key.secretKey ? decrypt(key.secretKey) : undefined,
48 | }))
49 | },
50 |
51 | // 获取单个 API 密钥
52 | getApiKeyById(id: number, userId: number): ApiKey | undefined {
53 | const apiKeys = this.getApiKeysByUserId(userId)
54 | return apiKeys.find((key) => key.id === id)
55 | },
56 |
57 | // 创建新的 API 密钥
58 | createApiKey(data: Omit): ApiKey {
59 | const allApiKeys = localStorage.getItem("api_key_manager_api_keys")
60 | const apiKeys = allApiKeys ? JSON.parse(allApiKeys) : []
61 |
62 | const newKey = {
63 | id: apiKeys.length > 0 ? Math.max(...apiKeys.map((k: ApiKey) => k.id)) + 1 : 1,
64 | ...data,
65 | createdAt: new Date().toISOString().split("T")[0],
66 | lastUsed: "-",
67 | }
68 |
69 | apiKeys.push(newKey)
70 | localStorage.setItem("api_key_manager_api_keys", JSON.stringify(apiKeys))
71 | return newKey
72 | },
73 |
74 | // 更新 API 密钥
75 | updateApiKey(id: number, userId: number, data: Partial): ApiKey | null {
76 | const allApiKeys = localStorage.getItem("api_key_manager_api_keys")
77 | const apiKeys = allApiKeys ? JSON.parse(allApiKeys) : []
78 |
79 | const keyIndex = apiKeys.findIndex((key: ApiKey) => key.id === id && key.userId === userId)
80 | if (keyIndex === -1) return null
81 |
82 | apiKeys[keyIndex] = {
83 | ...apiKeys[keyIndex],
84 | ...data,
85 | }
86 |
87 | localStorage.setItem("api_key_manager_api_keys", JSON.stringify(apiKeys))
88 | return apiKeys[keyIndex]
89 | },
90 |
91 | // 删除 API 密钥
92 | deleteApiKey(id: number, userId: number): boolean {
93 | const allApiKeys = localStorage.getItem("api_key_manager_api_keys")
94 | const apiKeys = allApiKeys ? JSON.parse(allApiKeys) : []
95 |
96 | const keyIndex = apiKeys.findIndex((key: ApiKey) => key.id === id && key.userId === userId)
97 | if (keyIndex === -1) return false
98 |
99 | apiKeys.splice(keyIndex, 1)
100 | localStorage.setItem("api_key_manager_api_keys", JSON.stringify(apiKeys))
101 | return true
102 | },
103 | }
104 |
105 |
--------------------------------------------------------------------------------
/lib/encryption.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 加密服务 - 用于加密和解密 API 密钥
3 | *
4 | * 使用简单的AES加密,通过localStorage存储
5 | */
6 |
7 | // 固定的加密密钥(仅用于演示)
8 | const ENCRYPTION_KEY = "api-key-manager-secret-key-12345"
9 |
10 | /**
11 | * 加密文本
12 | * @param text 要加密的文本
13 | * @returns 加密后的文本
14 | */
15 | export function encrypt(text: string): string {
16 | if (typeof window === "undefined") return text
17 |
18 | try {
19 | // 简单的AES加密实现
20 | // 在实际项目中,可以使用更复杂的加密库
21 | return btoa(
22 | Array.from(text)
23 | .map((char) => String.fromCharCode(char.charCodeAt(0) ^ ENCRYPTION_KEY.charCodeAt(0)))
24 | .join(""),
25 | )
26 | } catch (error) {
27 | console.error("加密失败:", error)
28 | return text
29 | }
30 | }
31 |
32 | /**
33 | * 解密文本
34 | * @param encryptedText 加密的文本
35 | * @returns 解密后的原始文本
36 | */
37 | export function decrypt(encryptedText: string): string {
38 | if (typeof window === "undefined") return encryptedText
39 |
40 | try {
41 | // 简单的AES解密实现
42 | return Array.from(atob(encryptedText))
43 | .map((char) => String.fromCharCode(char.charCodeAt(0) ^ ENCRYPTION_KEY.charCodeAt(0)))
44 | .join("")
45 | } catch (error) {
46 | console.error("解密失败:", error)
47 | return encryptedText
48 | }
49 | }
50 |
51 | /**
52 | * 测试加密和解密功能
53 | */
54 | export function testEncryption() {
55 | const originalText = "sk-1234567890abcdefghijklmnopqrstuvwxyz1234"
56 | console.log("原始文本:", originalText)
57 |
58 | const encrypted = encrypt(originalText)
59 | console.log("加密后:", encrypted)
60 |
61 | const decrypted = decrypt(encrypted)
62 | console.log("解密后:", decrypted)
63 |
64 | console.log("解密是否成功:", originalText === decrypted)
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/lib/error-handler.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 全局错误处理器
3 | * 用于在应用中统一处理和显示错误信息
4 | */
5 |
6 | /**
7 | * 显示应用错误提示
8 | * @param title 错误标题
9 | * @param message 错误详细信息
10 | */
11 | export function showAppError(title: string, message: string): void {
12 | if (typeof window === "undefined") return
13 |
14 | // 创建自定义错误事件
15 | const errorEvent = new CustomEvent("app-error", {
16 | detail: {
17 | title,
18 | message,
19 | },
20 | })
21 |
22 | // 触发错误事件
23 | window.dispatchEvent(errorEvent)
24 | }
25 |
26 | /**
27 | * 处理API密钥相关的错误
28 | * @param error 捕获的错误对象
29 | * @param operation 正在执行的操作
30 | */
31 | export function handleApiKeyError(error: unknown, operation: string): void {
32 | console.error(`API密钥操作失败 (${operation}):`, error)
33 |
34 | // 判断错误类型并提供相应的错误信息
35 | let title = "API密钥操作失败"
36 | let message = `在${operation}过程中发生错误`
37 |
38 | if (error instanceof Error) {
39 | if (error.message.includes("decrypt") || error.message.includes("encrypt")) {
40 | title = "加密/解密错误"
41 | message = "API密钥包含不支持的字符(如中文字符),请仅使用ASCII字符"
42 | } else if (error.message.includes("URL")) {
43 | title = "URL错误"
44 | message = "提供的URL格式不正确,请检查充值URL或API基础URL"
45 | } else if (error.message.includes("network") || error.message.includes("connect")) {
46 | title = "网络连接错误"
47 | message = "无法连接到API服务器,请检查网络连接或API地址"
48 | } else {
49 | message = error.message
50 | }
51 | }
52 |
53 | showAppError(title, message)
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/lib/i18n/language-context.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
4 | import { type Language, type TranslationKey, getTranslations } from "./translations"
5 |
6 | // 定义语言上下文类型
7 | interface LanguageContextType {
8 | language: Language
9 | setLanguage: (language: Language) => void
10 | t: (key: TranslationKey) => string
11 | }
12 |
13 | // 创建语言上下文
14 | const LanguageContext = createContext(undefined)
15 |
16 | // 语言提供者组件
17 | interface LanguageProviderProps {
18 | children: ReactNode
19 | defaultLanguage?: Language
20 | }
21 |
22 | export function LanguageProvider({ children, defaultLanguage = "zh-CN" }: LanguageProviderProps) {
23 | // 尝试从本地存储获取语言设置,如果没有则使用默认语言
24 | const [language, setLanguageState] = useState(() => {
25 | if (typeof window !== "undefined") {
26 | const savedLanguage = localStorage.getItem("language") as Language
27 | return savedLanguage || defaultLanguage
28 | }
29 | return defaultLanguage
30 | })
31 |
32 | // 设置语言并保存到本地存储
33 | const setLanguage = (newLanguage: Language) => {
34 | setLanguageState(newLanguage)
35 | if (typeof window !== "undefined") {
36 | localStorage.setItem("language", newLanguage)
37 | }
38 | }
39 |
40 | // 翻译函数
41 | const t = (key: TranslationKey): string => {
42 | const translations = getTranslations(language)
43 | return translations[key] || key
44 | }
45 |
46 | // 当语言变化时,更新文档的语言属性
47 | useEffect(() => {
48 | if (typeof window !== "undefined") {
49 | document.documentElement.lang = language
50 | }
51 | }, [language])
52 |
53 | return {children}
54 | }
55 |
56 | // 使用语言的钩子
57 | export function useLanguage() {
58 | const context = useContext(LanguageContext)
59 | if (context === undefined) {
60 | throw new Error("useLanguage must be used within a LanguageProvider")
61 | }
62 | return context
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/lib/storage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 本地存储服务 - 使用localStorage替代数据库
3 | * 注意:这种方式只适合小型应用和演示项目
4 | */
5 |
6 | // 在文件顶部添加导入
7 | import { encrypt, decrypt } from "@/lib/encryption"
8 |
9 | // 用户类型
10 | export interface User {
11 | id: number
12 | username: string
13 | password: string
14 | email: string
15 | created_at: string
16 | }
17 |
18 | // API密钥类型
19 | export interface ApiKey {
20 | id: number
21 | userId: number
22 | name: string
23 | key: string
24 | type: "apikey" | "complex"
25 | provider: string
26 | rechargeUrl?: string // 替代原来的 model 字段
27 | appId?: string
28 | secretKey?: string
29 | baseUrl: string
30 | createdAt: string
31 | lastUsed: string
32 | }
33 |
34 | // 修改 SystemSettings 接口,移除 encryptionLevel
35 | export interface SystemSettings {
36 | allowRegistration: boolean
37 | defaultKeyType: "apikey" | "complex"
38 | }
39 |
40 | // 更新默认设置
41 | const defaultSettings: SystemSettings = {
42 | allowRegistration: false,
43 | defaultKeyType: "apikey",
44 | }
45 |
46 | // 初始化默认数据
47 | const defaultUser: User = {
48 | id: 1,
49 | username: "admin",
50 | password: "password", // 实际项目中应该使用加密密码
51 | email: "admin@example.com",
52 | created_at: "2024-03-09",
53 | }
54 |
55 | // 简化为两个演示密钥
56 | const defaultApiKeys: ApiKey[] = [
57 | {
58 | id: 1,
59 | userId: 1,
60 | name: "OpenAI API (演示)",
61 | key: "sk-demo12345678abcdefghijklmnopqrstuvwxyz",
62 | type: "apikey",
63 | provider: "OpenAI",
64 | rechargeUrl: "https://api.example.com",
65 | baseUrl: "https://api.example.com",
66 | createdAt: "2024-03-15",
67 | lastUsed: "-",
68 | },
69 | {
70 | id: 2,
71 | userId: 1,
72 | name: "百度文心一言 (演示)",
73 | key: "demo_api_key_12345678",
74 | type: "complex",
75 | provider: "Baidu",
76 | appId: "demo_app_id_12345",
77 | secretKey: "demo_secret_key_12345",
78 | rechargeUrl: "https://console.bce.baidu.com/billing/#/billing/cbm/recharge",
79 | baseUrl: "https://api.example.com",
80 | createdAt: "2024-03-15",
81 | lastUsed: "-",
82 | },
83 | ]
84 |
85 | // 存储键名
86 | const STORAGE_KEYS = {
87 | USERS: "api_key_manager_users",
88 | API_KEYS: "api_key_manager_api_keys",
89 | SETTINGS: "api_key_manager_settings",
90 | }
91 |
92 | // 修改 initStorage 函数,确保在客户端环境中执行
93 | export function initStorage() {
94 | if (typeof window === "undefined") return
95 |
96 | // 只在客户端初始化
97 | if (!localStorage.getItem(STORAGE_KEYS.USERS)) {
98 | localStorage.setItem(STORAGE_KEYS.USERS, JSON.stringify([defaultUser]))
99 | }
100 |
101 | if (!localStorage.getItem(STORAGE_KEYS.API_KEYS)) {
102 | // 加密默认API密钥
103 | const encryptedKeys = defaultApiKeys.map((key) => ({
104 | ...key,
105 | key: encrypt(key.key),
106 | appId: key.appId ? encrypt(key.appId) : undefined,
107 | secretKey: key.secretKey ? encrypt(key.secretKey) : undefined,
108 | }))
109 | localStorage.setItem(STORAGE_KEYS.API_KEYS, JSON.stringify(encryptedKeys))
110 | }
111 |
112 | if (!localStorage.getItem(STORAGE_KEYS.SETTINGS)) {
113 | localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(defaultSettings))
114 | }
115 | }
116 |
117 | // 用户相关操作
118 | export const userStorage = {
119 | // 获取所有用户
120 | getUsers(): User[] {
121 | if (typeof window === "undefined") return []
122 | const users = localStorage.getItem(STORAGE_KEYS.USERS)
123 | return users ? JSON.parse(users) : []
124 | },
125 |
126 | // 根据用户名获取用户
127 | getUserByUsername(username: string): User | undefined {
128 | const users = this.getUsers()
129 | return users.find((user) => user.username === username)
130 | },
131 |
132 | // 创建新用户 - 保留但不使用
133 | createUser(username: string, password: string, email: string): User {
134 | const users = this.getUsers()
135 | const newUser = {
136 | id: users.length > 0 ? Math.max(...users.map((u) => u.id)) + 1 : 1,
137 | username,
138 | password,
139 | email,
140 | created_at: new Date().toISOString().split("T")[0],
141 | }
142 | users.push(newUser)
143 | localStorage.setItem(STORAGE_KEYS.USERS, JSON.stringify(users))
144 | return newUser
145 | },
146 |
147 | // 更新用户信息
148 | updateUser(userId: number, data: Partial): User | null {
149 | const users = this.getUsers()
150 | const userIndex = users.findIndex((user) => user.id === userId)
151 | if (userIndex === -1) return null
152 |
153 | users[userIndex] = {
154 | ...users[userIndex],
155 | ...data,
156 | }
157 | localStorage.setItem(STORAGE_KEYS.USERS, JSON.stringify(users))
158 | return users[userIndex]
159 | },
160 | }
161 |
162 | // API 密钥相关操作
163 | export const apiKeyStorage = {
164 | // 获取用户的所有 API 密钥
165 | getApiKeysByUserId(userId: number): ApiKey[] {
166 | if (typeof window === "undefined") return []
167 | const apiKeys = localStorage.getItem(STORAGE_KEYS.API_KEYS)
168 | const keys = apiKeys ? JSON.parse(apiKeys) : []
169 | const userKeys = keys.filter((key: ApiKey) => key.userId === userId)
170 |
171 | // 返回解密后的密钥
172 | return userKeys.map((key: ApiKey) => ({
173 | ...key,
174 | key: key.key ? decrypt(key.key) : "",
175 | appId: key.appId ? decrypt(key.appId) : undefined,
176 | secretKey: key.secretKey ? decrypt(key.secretKey) : undefined,
177 | }))
178 | },
179 |
180 | // 获取单个 API 密钥
181 | getApiKeyById(id: number, userId: number): ApiKey | undefined {
182 | const apiKeys = this.getApiKeysByUserId(userId)
183 | return apiKeys.find((key) => key.id === id)
184 | },
185 |
186 | // 创建新的 API 密钥
187 | createApiKey(data: Omit): ApiKey {
188 | const allApiKeys = localStorage.getItem(STORAGE_KEYS.API_KEYS)
189 | const apiKeys = allApiKeys ? JSON.parse(allApiKeys) : []
190 |
191 | const newKey = {
192 | id: apiKeys.length > 0 ? Math.max(...apiKeys.map((k: ApiKey) => k.id)) + 1 : 1,
193 | ...data,
194 | createdAt: new Date().toISOString().split("T")[0],
195 | lastUsed: "-",
196 | }
197 |
198 | apiKeys.push(newKey)
199 | localStorage.setItem(STORAGE_KEYS.API_KEYS, JSON.stringify(apiKeys))
200 | return newKey
201 | },
202 |
203 | // 更新 API 密钥
204 | updateApiKey(id: number, userId: number, data: Partial): ApiKey | null {
205 | const allApiKeys = localStorage.getItem(STORAGE_KEYS.API_KEYS)
206 | const apiKeys = allApiKeys ? JSON.parse(allApiKeys) : []
207 |
208 | const keyIndex = apiKeys.findIndex((key: ApiKey) => key.id === id && key.userId === userId)
209 | if (keyIndex === -1) return null
210 |
211 | apiKeys[keyIndex] = {
212 | ...apiKeys[keyIndex],
213 | ...data,
214 | }
215 |
216 | localStorage.setItem(STORAGE_KEYS.API_KEYS, JSON.stringify(apiKeys))
217 | return apiKeys[keyIndex]
218 | },
219 |
220 | // 删除 API 密钥
221 | deleteApiKey(id: number, userId: number): boolean {
222 | const allApiKeys = localStorage.getItem(STORAGE_KEYS.API_KEYS)
223 | const apiKeys = allApiKeys ? JSON.parse(allApiKeys) : []
224 |
225 | const keyIndex = apiKeys.findIndex((key: ApiKey) => key.id === id && key.userId === userId)
226 | if (keyIndex === -1) return false
227 |
228 | apiKeys.splice(keyIndex, 1)
229 | localStorage.setItem(STORAGE_KEYS.API_KEYS, JSON.stringify(apiKeys))
230 | return true
231 | },
232 | }
233 |
234 | // 系统设置相关操作
235 | export const settingsStorage = {
236 | // 获取系统设置
237 | getSystemSettings(): SystemSettings {
238 | if (typeof window === "undefined") return defaultSettings
239 | const settings = localStorage.getItem(STORAGE_KEYS.SETTINGS)
240 | return settings ? JSON.parse(settings) : defaultSettings
241 | },
242 |
243 | // 更新系统设置
244 | updateSystemSettings(data: Partial): SystemSettings {
245 | const currentSettings = this.getSystemSettings()
246 | const updatedSettings = {
247 | ...currentSettings,
248 | ...data,
249 | }
250 | localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(updatedSettings))
251 | return updatedSettings
252 | },
253 | }
254 |
255 |
--------------------------------------------------------------------------------
/lib/storage/settings-storage.ts:
--------------------------------------------------------------------------------
1 | // lib/storage/settings-storage.ts
2 |
3 | import type { SystemSettings } from "@/lib/storage"
4 |
5 | // 存储键名
6 | const STORAGE_KEYS = {
7 | SETTINGS: "api_key_manager_settings",
8 | }
9 |
10 | // 系统设置相关操作
11 | export const settingsStorage = {
12 | // 获取系统设置
13 | getSystemSettings(): SystemSettings {
14 | if (typeof window === "undefined") return { allowRegistration: false, defaultKeyType: "apikey" }
15 | const settings = localStorage.getItem(STORAGE_KEYS.SETTINGS)
16 | return settings ? JSON.parse(settings) : { allowRegistration: false, defaultKeyType: "apikey" }
17 | },
18 |
19 | // 更新系统设置
20 | updateSystemSettings(data: Partial): SystemSettings {
21 | const currentSettings = this.getSystemSettings()
22 | const updatedSettings = {
23 | ...currentSettings,
24 | ...data,
25 | }
26 | localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(updatedSettings))
27 | return updatedSettings
28 | },
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | let userConfig = undefined
2 | try {
3 | userConfig = await import('./v0-user-next.config')
4 | } catch (e) {
5 | // ignore error
6 | }
7 |
8 | /** @type {import('next').NextConfig} */
9 | const nextConfig = {
10 | eslint: {
11 | ignoreDuringBuilds: true,
12 | },
13 | typescript: {
14 | ignoreBuildErrors: true,
15 | },
16 | images: {
17 | unoptimized: true,
18 | },
19 | experimental: {
20 | webpackBuildWorker: true,
21 | parallelServerBuildTraces: true,
22 | parallelServerCompiles: true,
23 | },
24 | }
25 |
26 | mergeConfig(nextConfig, userConfig)
27 |
28 | function mergeConfig(nextConfig, userConfig) {
29 | if (!userConfig) {
30 | return
31 | }
32 |
33 | for (const key in userConfig) {
34 | if (
35 | typeof nextConfig[key] === 'object' &&
36 | !Array.isArray(nextConfig[key])
37 | ) {
38 | nextConfig[key] = {
39 | ...nextConfig[key],
40 | ...userConfig[key],
41 | }
42 | } else {
43 | nextConfig[key] = userConfig[key]
44 | }
45 | }
46 | }
47 |
48 | export default nextConfig
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api-key-manager",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@radix-ui/react-accordion": "^1.1.2",
13 | "@radix-ui/react-alert-dialog": "^1.0.5",
14 | "@radix-ui/react-avatar": "^1.0.4",
15 | "@radix-ui/react-checkbox": "^1.0.4",
16 | "@radix-ui/react-collapsible": "^1.0.3",
17 | "@radix-ui/react-context-menu": "^2.1.5",
18 | "@radix-ui/react-dialog": "^1.0.5",
19 | "@radix-ui/react-dropdown-menu": "^2.0.6",
20 | "@radix-ui/react-hover-card": "^1.0.7",
21 | "@radix-ui/react-label": "^2.0.2",
22 | "@radix-ui/react-menubar": "^1.0.4",
23 | "@radix-ui/react-navigation-menu": "^1.1.4",
24 | "@radix-ui/react-popover": "^1.0.7",
25 | "@radix-ui/react-progress": "^1.0.3",
26 | "@radix-ui/react-radio-group": "^1.1.3",
27 | "@radix-ui/react-scroll-area": "^1.0.5",
28 | "@radix-ui/react-select": "^2.0.0",
29 | "@radix-ui/react-separator": "^1.0.3",
30 | "@radix-ui/react-slider": "^1.1.2",
31 | "@radix-ui/react-slot": "^1.0.2",
32 | "@radix-ui/react-switch": "^1.0.3",
33 | "@radix-ui/react-tabs": "^1.0.4",
34 | "@radix-ui/react-toast": "^1.1.5",
35 | "@radix-ui/react-toggle": "^1.0.3",
36 | "@radix-ui/react-tooltip": "^1.0.7",
37 | "class-variance-authority": "^0.7.0",
38 | "clsx": "^2.1.0",
39 | "lucide-react": "^0.323.0",
40 | "next": "14.1.0",
41 | "next-themes": "^0.2.1",
42 | "react": "^18.2.0",
43 | "react-dom": "^18.2.0",
44 | "tailwind-merge": "^2.2.1",
45 | "tailwindcss-animate": "^1.0.7"
46 | },
47 | "devDependencies": {
48 | "@types/node": "^20.11.17",
49 | "@types/react": "^18.2.55",
50 | "@types/react-dom": "^18.2.19",
51 | "autoprefixer": "^10.4.17",
52 | "eslint": "^8.56.0",
53 | "eslint-config-next": "14.1.0",
54 | "postcss": "^8.4.35",
55 | "tailwindcss": "^3.4.1",
56 | "typescript": "^5.3.3"
57 | }
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/image/Dashboard-cn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/Dashboard-cn.png
--------------------------------------------------------------------------------
/public/image/Dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/Dashboard.png
--------------------------------------------------------------------------------
/public/image/api-key-cn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/api-key-cn.png
--------------------------------------------------------------------------------
/public/image/api-key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/api-key.png
--------------------------------------------------------------------------------
/public/image/complex-cn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/complex-cn.png
--------------------------------------------------------------------------------
/public/image/newkey-complex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/newkey-complex.png
--------------------------------------------------------------------------------
/public/image/newkey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/newkey.png
--------------------------------------------------------------------------------
/public/image/newkeyi-cn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/newkeyi-cn.png
--------------------------------------------------------------------------------
/public/image/sys-set-cn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/sys-set-cn.png
--------------------------------------------------------------------------------
/public/image/sys-set.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/randomAndre/api-key-manager/f20747e4f78a0c3f8db5e28157ad0b9a5abc34f5/public/image/sys-set.png
--------------------------------------------------------------------------------
/public/placeholder-logo.png:
--------------------------------------------------------------------------------
1 | �PNG
2 |
3 |
IHDR � M�� 0PLTE Z? tRNS � �@��`P0p���w �IDATx��ؽJ3Q�7'��%�|?� ���E�l�7���(X�D������w`����[�*t����D���mD�}��4;;�DDDDDDDDDDDD_�_İ��!�y�`�_�:��;Ļ�'|� ��;.I"����3*5����J�1�� �T��FI�� ��=��3܃�2~�b���0��U9\��]�4�#w0��Gt\&1�?21,���o!e�m��ĻR�����5�� ؽAJ�9��R)�5�0.FFASaǃ�T�#|�K���I�������1�
4 | M������N"��$����G�V�T���T^^��A�$S��h(�������G]co"J^^�'�=���%� �W�6Ы�W��w�a�߇*�^^�YG�c���`'F����������������^ 5_�,�S�% IEND�B`�
--------------------------------------------------------------------------------
/public/placeholder-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/placeholder-user.jpg:
--------------------------------------------------------------------------------
1 | ���� JFIF �� C
2 |
3 |
4 | $ &%# #"(-90(*6+"#2D26;=@@@&0FKE>J9?@=�� C
=)#)==================================================�� � � �� �� �� ـ |�r4�-� ̈"x�'�0 �Í��8�H�N� q�����Q�� �����V�`= �($q"_��
5 | �S8�P��0 VFbP��!
6 | Io40 ��[?p #�|�@ !.E� 3��4p Bq �Z
s � �� C AQR�!1@Ua��02Tq���56cps�� "#$PS����� ? ��R���,�����
7 | �n�k��n8rZ�����9Vv� �V��ms$9zWʏh�-+@Z�2�PGE������EY9��i�Ͻ�S ��O��Ȕ��_I��W髵�}�����B�ՎT��>%r�[e/,W�D}��D�>b�e>�v�Z�p&�*VS��V�sV�c �:��~K������� C��:��k�'An|ʶ�}\��C� �f����a�;�h��J����q!i=���"�q�NF�IZ�`�wĝ5hAj�
8 | �RXl䎉�lk���@I�%l��Ն���FDY-����Eq�i����O�I�_�2b�lNj�Yu��k���AO����٣��ܭ�n��cam�jN�j���VL�}�;��oކ6��շs��,���ք���l�i����l{I�O��(!%J $���n�-@G����n��ܮi!�괁G�:�^��n�g3l�F%���9]�Pq��)�:���@�*ɍmׅ�VLY'�s+�z���V�m �J9��S�_���#��;�����NJ!5�#�q\�M@��@�]yz�����A;e�k��@� s�^���G����\�5F��(�� S��Ly���c�i8�����o�8T��i�N��7D����t-� p�3`r� q
r�;|�.��bTG��[i H��͚-�
9 | ��Oj�H����M�ؒFE�{�3X�n���e� �R3/�~�����
10 | �� a����!�j&@^r�����Y�������l�Z? �7땵��)ki�w��\.�u�����X��\.�u�����X��\.�u�����X��\.�u��p�M����o(N��3�Vg�����Z�s��%�\�]q}d�k\_Y5���MG�����Q��q}d�k\_Y5���MG�����Q��q}d�kV|5���8���//���� ��� ? �� ��� ? ��
--------------------------------------------------------------------------------
/public/placeholder.jpg:
--------------------------------------------------------------------------------
1 | ���� JFIF H H �� �Exif MM * J R( �i Z H H � � � �� 8Photoshop 3.0 8BIM 8BIM% ��ُ �� ���B~�� ��
2 | �� � s !1"AQ2aq#� �B�R3�$b0�r�C�4��S@%c5�s�PD���&T6d�t�`҄�p�'E7e�Uu��Å��Fv��GVf�
3 | ()*89:HIJWXYZghijwxyz�����������������������������������������������������������
4 | �� � � ! 1A0"2Q@3#aBqR4�P$��C�b5S��%`�D�r��c6p&ET�'��
5 | ()*789:FGHIJUVWXYZdefghijstuvwxyz����������������������������������������������������������������������������� C
6 |
7 |
8 |
")$+*($''-2@7-0=0''8L9=CEHIH+6OUNFT@GHE�� C
!!E.'.EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE�� �k�� �� ?�� ?�� ?�� 3 !1AQaq��������� 0@P`p��������� ?!��� �� 3 !1AQa q𑁡�����0@P`p��������� ?��� ?��� ?���
--------------------------------------------------------------------------------
/public/placeholder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer utilities {
10 | .text-balance {
11 | text-wrap: balance;
12 | }
13 | }
14 |
15 | @layer base {
16 | :root {
17 | --background: 0 0% 100%;
18 | --foreground: 0 0% 3.9%;
19 | --card: 0 0% 100%;
20 | --card-foreground: 0 0% 3.9%;
21 | --popover: 0 0% 100%;
22 | --popover-foreground: 0 0% 3.9%;
23 | --primary: 0 0% 9%;
24 | --primary-foreground: 0 0% 98%;
25 | --secondary: 0 0% 96.1%;
26 | --secondary-foreground: 0 0% 9%;
27 | --muted: 0 0% 96.1%;
28 | --muted-foreground: 0 0% 45.1%;
29 | --accent: 0 0% 96.1%;
30 | --accent-foreground: 0 0% 9%;
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 0 0% 98%;
33 | --border: 0 0% 89.8%;
34 | --input: 0 0% 89.8%;
35 | --ring: 0 0% 3.9%;
36 | --chart-1: 12 76% 61%;
37 | --chart-2: 173 58% 39%;
38 | --chart-3: 197 37% 24%;
39 | --chart-4: 43 74% 66%;
40 | --chart-5: 27 87% 67%;
41 | --radius: 0.5rem;
42 | --sidebar-background: 0 0% 98%;
43 | --sidebar-foreground: 240 5.3% 26.1%;
44 | --sidebar-primary: 240 5.9% 10%;
45 | --sidebar-primary-foreground: 0 0% 98%;
46 | --sidebar-accent: 240 4.8% 95.9%;
47 | --sidebar-accent-foreground: 240 5.9% 10%;
48 | --sidebar-border: 220 13% 91%;
49 | --sidebar-ring: 217.2 91.2% 59.8%;
50 | }
51 | .dark {
52 | --background: 0 0% 3.9%;
53 | --foreground: 0 0% 98%;
54 | --card: 0 0% 3.9%;
55 | --card-foreground: 0 0% 98%;
56 | --popover: 0 0% 3.9%;
57 | --popover-foreground: 0 0% 98%;
58 | --primary: 0 0% 98%;
59 | --primary-foreground: 0 0% 9%;
60 | --secondary: 0 0% 14.9%;
61 | --secondary-foreground: 0 0% 98%;
62 | --muted: 0 0% 14.9%;
63 | --muted-foreground: 0 0% 63.9%;
64 | --accent: 0 0% 14.9%;
65 | --accent-foreground: 0 0% 98%;
66 | --destructive: 0 62.8% 30.6%;
67 | --destructive-foreground: 0 0% 98%;
68 | --border: 0 0% 14.9%;
69 | --input: 0 0% 14.9%;
70 | --ring: 0 0% 83.1%;
71 | --chart-1: 220 70% 50%;
72 | --chart-2: 160 60% 45%;
73 | --chart-3: 30 80% 55%;
74 | --chart-4: 280 65% 60%;
75 | --chart-5: 340 75% 55%;
76 | --sidebar-background: 240 5.9% 10%;
77 | --sidebar-foreground: 240 4.8% 95.9%;
78 | --sidebar-primary: 224.3 76.3% 48%;
79 | --sidebar-primary-foreground: 0 0% 100%;
80 | --sidebar-accent: 240 3.7% 15.9%;
81 | --sidebar-accent-foreground: 240 4.8% 95.9%;
82 | --sidebar-border: 240 3.7% 15.9%;
83 | --sidebar-ring: 217.2 91.2% 59.8%;
84 | }
85 | }
86 |
87 | @layer base {
88 | * {
89 | @apply border-border;
90 | }
91 | body {
92 | @apply bg-background text-foreground;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{ts,tsx}",
7 | "./components/**/*.{ts,tsx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}",
10 | "*.{js,ts,jsx,tsx,mdx}",
11 | ],
12 | prefix: "",
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: "2rem",
17 | screens: {
18 | "2xl": "1400px",
19 | },
20 | },
21 | extend: {
22 | colors: {
23 | border: "hsl(var(--border))",
24 | input: "hsl(var(--input))",
25 | ring: "hsl(var(--ring))",
26 | background: "hsl(var(--background))",
27 | foreground: "hsl(var(--foreground))",
28 | primary: {
29 | DEFAULT: "hsl(var(--primary))",
30 | foreground: "hsl(var(--primary-foreground))",
31 | },
32 | secondary: {
33 | DEFAULT: "hsl(var(--secondary))",
34 | foreground: "hsl(var(--secondary-foreground))",
35 | },
36 | destructive: {
37 | DEFAULT: "hsl(var(--destructive))",
38 | foreground: "hsl(var(--destructive-foreground))",
39 | },
40 | muted: {
41 | DEFAULT: "hsl(var(--muted))",
42 | foreground: "hsl(var(--muted-foreground))",
43 | },
44 | accent: {
45 | DEFAULT: "hsl(var(--accent))",
46 | foreground: "hsl(var(--accent-foreground))",
47 | },
48 | popover: {
49 | DEFAULT: "hsl(var(--popover))",
50 | foreground: "hsl(var(--popover-foreground))",
51 | },
52 | card: {
53 | DEFAULT: "hsl(var(--card))",
54 | foreground: "hsl(var(--card-foreground))",
55 | },
56 | },
57 | borderRadius: {
58 | lg: "var(--radius)",
59 | md: "calc(var(--radius) - 2px)",
60 | sm: "calc(var(--radius) - 4px)",
61 | },
62 | keyframes: {
63 | "accordion-down": {
64 | from: { height: "0" },
65 | to: { height: "var(--radix-accordion-content-height)" },
66 | },
67 | "accordion-up": {
68 | from: { height: "var(--radix-accordion-content-height)" },
69 | to: { height: "0" },
70 | },
71 | "collapsible-down": {
72 | from: { height: "0" },
73 | to: { height: "var(--radix-collapsible-content-height)" },
74 | },
75 | "collapsible-up": {
76 | from: { height: "var(--radix-collapsible-content-height)" },
77 | to: { height: "0" },
78 | },
79 | },
80 | animation: {
81 | "accordion-down": "accordion-down 0.2s ease-out",
82 | "accordion-up": "accordion-up 0.2s ease-out",
83 | "collapsible-down": "collapsible-down 0.2s ease-out",
84 | "collapsible-up": "collapsible-up 0.2s ease-out",
85 | },
86 | },
87 | },
88 | plugins: [require("tailwindcss-animate")],
89 | } satisfies Config
90 |
91 | export default config
92 |
93 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "target": "ES6",
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------