diff --git a/.gitignore b/.gitignore
index 5ef6a52..f390d12 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+/src/generated/prisma
diff --git a/API文档.md b/API文档.md
new file mode 100644
index 0000000..00ada70
--- /dev/null
+++ b/API文档.md
@@ -0,0 +1,943 @@
+# CarrotSkin 后端 API 文档
+
+## 概述
+
+本文档总结了 CarrotSkin 后端 API,主要关注前端需要的接口,不包括 Yggdrasil 相关接口(除了更换 Yggdrasil 密码)。
+
+## 基础信息
+
+- **基础URL**: `/api/v1`
+- **认证方式**: JWT Bearer Token
+- **数据格式**: JSON
+- **字符编码**: UTF-8
+
+## 通用响应格式
+
+所有API响应都遵循以下格式:
+
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ // 具体数据内容
+ }
+}
+```
+
+分页响应格式:
+
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "list": [],
+ "total": 100,
+ "page": 1,
+ "page_size": 20,
+ "total_pages": 5
+ }
+}
+```
+
+## 认证相关 API
+
+### 1. 用户注册
+
+- **URL**: `POST /api/v1/auth/register`
+- **认证**: 无需认证
+- **请求参数**:
+```json
+{
+ "username": "newuser", // 用户名,3-50字符
+ "email": "user@example.com", // 邮箱地址
+ "password": "password123", // 密码,6-128字符
+ "verification_code": "123456", // 邮箱验证码,6位数字
+ "avatar": "https://example.com/avatar.png" // 可选,头像URL
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "注册成功",
+ "data": {
+ "token": "jwt_token_here",
+ "user_info": {
+ "id": 1,
+ "username": "newuser",
+ "email": "user@example.com",
+ "avatar": "https://example.com/avatar.png",
+ "points": 0,
+ "role": "user",
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+ }
+}
+```
+
+### 2. 用户登录
+
+- **URL**: `POST /api/v1/auth/login`
+- **认证**: 无需认证
+- **请求参数**:
+```json
+{
+ "username": "testuser", // 用户名或邮箱
+ "password": "password123" // 密码
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "登录成功",
+ "data": {
+ "token": "jwt_token_here",
+ "user_info": {
+ "id": 1,
+ "username": "testuser",
+ "email": "test@example.com",
+ "avatar": "https://example.com/avatar.png",
+ "points": 100,
+ "role": "user",
+ "status": 1,
+ "last_login_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+ }
+}
+```
+
+### 3. 发送验证码
+
+- **URL**: `POST /api/v1/auth/send-code`
+- **认证**: 无需认证
+- **请求参数**:
+```json
+{
+ "email": "user@example.com", // 邮箱地址
+ "type": "register" // 类型: register/reset_password/change_email
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "验证码已发送,请查收邮件",
+ "data": {
+ "message": "验证码已发送,请查收邮件"
+ }
+}
+```
+
+### 4. 重置密码
+
+- **URL**: `POST /api/v1/auth/reset-password`
+- **认证**: 无需认证
+- **请求参数**:
+```json
+{
+ "email": "user@example.com", // 邮箱地址
+ "verification_code": "123456", // 邮箱验证码
+ "new_password": "newpassword123" // 新密码
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "密码重置成功",
+ "data": {
+ "message": "密码重置成功"
+ }
+}
+```
+
+## 用户相关 API
+
+### 1. 获取用户信息
+
+- **URL**: `GET /api/v1/user/profile`
+- **认证**: 需要JWT认证
+- **请求参数**: 无
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "username": "testuser",
+ "email": "test@example.com",
+ "avatar": "https://example.com/avatar.png",
+ "points": 100,
+ "role": "user",
+ "status": 1,
+ "last_login_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 2. 更新用户信息
+
+- **URL**: `PUT /api/v1/user/profile`
+- **认证**: 需要JWT认证
+- **请求参数**:
+```json
+{
+ "avatar": "https://example.com/new-avatar.png", // 可选,新头像URL
+ "old_password": "oldpassword123", // 可选,修改密码时需要
+ "new_password": "newpassword123" // 可选,新密码
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "username": "testuser",
+ "email": "test@example.com",
+ "avatar": "https://example.com/new-avatar.png",
+ "points": 100,
+ "role": "user",
+ "status": 1,
+ "last_login_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 3. 生成头像上传URL
+
+- **URL**: `POST /api/v1/user/avatar/upload-url`
+- **认证**: 需要JWT认证
+- **请求参数**:
+```json
+{
+ "file_name": "avatar.png" // 文件名
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "post_url": "https://rustfs.example.com/avatars",
+ "form_data": {
+ "key": "user_1/xxx.png",
+ "policy": "base64_policy",
+ "x-amz-signature": "signature"
+ },
+ "avatar_url": "https://rustfs.example.com/avatars/user_1/xxx.png",
+ "expires_in": 900
+ }
+}
+```
+
+### 4. 更新头像URL
+
+- **URL**: `PUT /api/v1/user/avatar`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - Query参数: `avatar_url` - 头像URL
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "username": "testuser",
+ "email": "test@example.com",
+ "avatar": "https://example.com/new-avatar.png",
+ "points": 100,
+ "role": "user",
+ "status": 1,
+ "last_login_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 5. 更换邮箱
+
+- **URL**: `POST /api/v1/user/change-email`
+- **认证**: 需要JWT认证
+- **请求参数**:
+```json
+{
+ "new_email": "newemail@example.com", // 新邮箱地址
+ "verification_code": "123456" // 邮箱验证码
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "username": "testuser",
+ "email": "newemail@example.com",
+ "avatar": "https://example.com/avatar.png",
+ "points": 100,
+ "role": "user",
+ "status": 1,
+ "last_login_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 6. 重置Yggdrasil密码
+
+- **URL**: `POST /api/v1/user/yggdrasil-password/reset`
+- **认证**: 需要JWT认证
+- **请求参数**: 无
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "password": "new_yggdrasil_password"
+ }
+}
+```
+
+## 材质相关 API
+
+### 1. 搜索材质
+
+- **URL**: `GET /api/v1/texture`
+- **认证**: 无需认证
+- **请求参数**:
+ - Query参数:
+ - `keyword`: 搜索关键词
+ - `type`: 材质类型 (SKIN/CAPE)
+ - `public_only`: 是否只搜索公开材质 (true/false)
+ - `page`: 页码,默认1
+ - `page_size`: 每页数量,默认20
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "list": [
+ {
+ "id": 1,
+ "uploader_id": 1,
+ "name": "My Skin",
+ "description": "A cool skin",
+ "type": "SKIN",
+ "url": "https://rustfs.example.com/textures/xxx.png",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 2048,
+ "is_public": true,
+ "download_count": 100,
+ "favorite_count": 50,
+ "is_slim": false,
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+ ],
+ "total": 100,
+ "page": 1,
+ "page_size": 20,
+ "total_pages": 5
+ }
+}
+```
+
+### 2. 获取材质详情
+
+- **URL**: `GET /api/v1/texture/{id}`
+- **认证**: 无需认证
+- **请求参数**:
+ - 路径参数: `id` - 材质ID
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "uploader_id": 1,
+ "name": "My Skin",
+ "description": "A cool skin",
+ "type": "SKIN",
+ "url": "https://rustfs.example.com/textures/xxx.png",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 2048,
+ "is_public": true,
+ "download_count": 100,
+ "favorite_count": 50,
+ "is_slim": false,
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 3. 直接上传材质文件(推荐)
+
+- **URL**: `POST /api/v1/texture/upload`
+- **认证**: 需要JWT认证
+- **Content-Type**: `multipart/form-data`
+- **请求参数**:
+ - `file`: 材质文件(PNG格式,1KB-10MB)
+ - `name`: 材质名称(必填,1-100字符)
+ - `description`: 材质描述(可选,最多500字符)
+ - `type`: 材质类型(可选,默认SKIN,可选值:SKIN/CAPE)
+ - `is_public`: 是否公开(可选,默认false,true/false)
+ - `is_slim`: 是否为细臂模型(可选,默认false,true/false)
+- **说明**:
+ - 后端会自动计算文件的SHA256哈希值
+ - 如果已存在相同哈希的材质,会复用已存在的文件URL,不重复上传
+ - 允许多次上传相同哈希的材质(包括同一用户),每次都会创建新的数据库记录
+ - 文件存储路径格式:`{type}/{hash[:2]}/{hash[2:4]}/{hash}.png`
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "uploader_id": 1,
+ "name": "My Cool Skin",
+ "description": "A very cool skin",
+ "type": "SKIN",
+ "url": "https://rustfs.example.com/textures/skin/e3/b0/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.png",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 2048,
+ "is_public": true,
+ "download_count": 0,
+ "favorite_count": 0,
+ "is_slim": false,
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 4. 生成材质上传URL(兼容接口)
+
+- **URL**: `POST /api/v1/texture/upload-url`
+- **认证**: 需要JWT认证
+- **请求参数**:
+```json
+{
+ "file_name": "skin.png", // 文件名
+ "texture_type": "SKIN" // 材质类型: SKIN/CAPE
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "post_url": "https://rustfs.example.com/textures",
+ "form_data": {
+ "key": "user_1/skin/xxx.png",
+ "policy": "base64_policy",
+ "x-amz-signature": "signature"
+ },
+ "texture_url": "https://rustfs.example.com/textures/user_1/skin/xxx.png",
+ "expires_in": 900
+ }
+}
+```
+
+### 5. 创建材质记录(配合预签名URL使用)
+
+- **URL**: `POST /api/v1/texture`
+- **认证**: 需要JWT认证
+- **请求参数**:
+```json
+{
+ "name": "My Cool Skin", // 材质名称,1-100字符
+ "description": "A very cool skin", // 描述,最多500字符
+ "type": "SKIN", // 材质类型: SKIN/CAPE
+ "url": "https://rustfs.example.com/textures/user_1/skin/xxx.png", // 材质URL
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", // SHA256哈希
+ "size": 2048, // 文件大小(字节)
+ "is_public": true, // 是否公开
+ "is_slim": false // 是否为细臂模型(Alex)
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "uploader_id": 1,
+ "name": "My Cool Skin",
+ "description": "A very cool skin",
+ "type": "SKIN",
+ "url": "https://rustfs.example.com/textures/user_1/skin/xxx.png",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 2048,
+ "is_public": true,
+ "download_count": 0,
+ "favorite_count": 0,
+ "is_slim": false,
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 6. 更新材质
+
+- **URL**: `PUT /api/v1/texture/{id}`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - 路径参数: `id` - 材质ID
+ - 请求体:
+```json
+{
+ "name": "Updated Skin Name", // 可选,新名称
+ "description": "Updated description", // 可选,新描述
+ "is_public": false // 可选,是否公开
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "id": 1,
+ "uploader_id": 1,
+ "name": "Updated Skin Name",
+ "description": "Updated description",
+ "type": "SKIN",
+ "url": "https://rustfs.example.com/textures/user_1/skin/xxx.png",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 2048,
+ "is_public": false,
+ "download_count": 100,
+ "favorite_count": 50,
+ "is_slim": false,
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 7. 删除材质
+
+- **URL**: `DELETE /api/v1/texture/{id}`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - 路径参数: `id` - 材质ID
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": null
+}
+```
+
+### 8. 切换收藏状态
+
+- **URL**: `POST /api/v1/texture/{id}/favorite`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - 路径参数: `id` - 材质ID
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "is_favorited": true
+ }
+}
+```
+
+### 9. 获取用户上传的材质列表
+
+- **URL**: `GET /api/v1/texture/my`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - Query参数:
+ - `page`: 页码,默认1
+ - `page_size`: 每页数量,默认20
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "list": [
+ {
+ "id": 1,
+ "uploader_id": 1,
+ "name": "My Skin",
+ "description": "A cool skin",
+ "type": "SKIN",
+ "url": "https://rustfs.example.com/textures/xxx.png",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 2048,
+ "is_public": true,
+ "download_count": 100,
+ "favorite_count": 50,
+ "is_slim": false,
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+ ],
+ "total": 50,
+ "page": 1,
+ "page_size": 20,
+ "total_pages": 3
+ }
+}
+```
+
+### 10. 获取用户收藏的材质列表
+
+- **URL**: `GET /api/v1/texture/favorites`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - Query参数:
+ - `page`: 页码,默认1
+ - `page_size`: 每页数量,默认20
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "list": [
+ {
+ "id": 1,
+ "uploader_id": 2,
+ "name": "Cool Skin",
+ "description": "A very cool skin",
+ "type": "SKIN",
+ "url": "https://rustfs.example.com/textures/xxx.png",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 2048,
+ "is_public": true,
+ "download_count": 100,
+ "favorite_count": 50,
+ "is_slim": false,
+ "status": 1,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+ ],
+ "total": 30,
+ "page": 1,
+ "page_size": 20,
+ "total_pages": 2
+ }
+}
+```
+
+## 档案相关 API
+
+### 1. 创建档案
+
+- **URL**: `POST /api/v1/profile`
+- **认证**: 需要JWT认证
+- **请求参数**:
+```json
+{
+ "name": "PlayerName" // 角色名,1-16字符
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "uuid": "550e8400-e29b-41d4-a716-446655440000",
+ "user_id": 1,
+ "name": "PlayerName",
+ "skin_id": null,
+ "cape_id": null,
+ "is_active": false,
+ "last_used_at": null,
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 2. 获取档案列表
+
+- **URL**: `GET /api/v1/profile`
+- **认证**: 需要JWT认证
+- **请求参数**: 无
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": [
+ {
+ "uuid": "550e8400-e29b-41d4-a716-446655440000",
+ "user_id": 1,
+ "name": "PlayerName",
+ "skin_id": 1,
+ "cape_id": 2,
+ "is_active": true,
+ "last_used_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+ ]
+}
+```
+
+### 3. 获取档案详情
+
+- **URL**: `GET /api/v1/profile/{uuid}`
+- **认证**: 无需认证
+- **请求参数**:
+ - 路径参数: `uuid` - 档案UUID
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "uuid": "550e8400-e29b-41d4-a716-446655440000",
+ "user_id": 1,
+ "name": "PlayerName",
+ "skin_id": 1,
+ "cape_id": 2,
+ "is_active": true,
+ "last_used_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 4. 更新档案
+
+- **URL**: `PUT /api/v1/profile/{uuid}`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - 路径参数: `uuid` - 档案UUID
+ - 请求体:
+```json
+{
+ "name": "NewPlayerName", // 可选,新角色名
+ "skin_id": 1, // 可选,皮肤ID
+ "cape_id": 2 // 可选,披风ID
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "uuid": "550e8400-e29b-41d4-a716-446655440000",
+ "user_id": 1,
+ "name": "NewPlayerName",
+ "skin_id": 1,
+ "cape_id": 2,
+ "is_active": true,
+ "last_used_at": "2025-10-01T12:00:00Z",
+ "created_at": "2025-10-01T10:00:00Z",
+ "updated_at": "2025-10-01T10:00:00Z"
+ }
+}
+```
+
+### 5. 删除档案
+
+- **URL**: `DELETE /api/v1/profile/{uuid}`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - 路径参数: `uuid` - 档案UUID
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "message": "删除成功"
+ }
+}
+```
+
+### 6. 设置活跃档案
+
+- **URL**: `POST /api/v1/profile/{uuid}/activate`
+- **认证**: 需要JWT认证
+- **请求参数**:
+ - 路径参数: `uuid` - 档案UUID
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "message": "设置成功"
+ }
+}
+```
+
+## 验证码相关 API
+
+### 1. 生成验证码
+
+- **URL**: `GET /api/v1/captcha/generate`
+- **认证**: 无需认证
+- **请求参数**: 无
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "data": {
+ "masterImage": "base64_encoded_master_image",
+ "tileImage": "base64_encoded_tile_image",
+ "captchaId": "captcha_id_here",
+ "y": 100
+ }
+}
+```
+
+### 2. 验证验证码
+
+- **URL**: `POST /api/v1/captcha/verify`
+- **认证**: 无需认证
+- **请求参数**:
+```json
+{
+ "captchaId": "captcha_id_here",
+ "dx": 150 // 滑动距离
+}
+```
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "msg": "验证成功"
+}
+```
+
+## 系统相关 API
+
+### 1. 获取系统配置
+
+- **URL**: `GET /api/v1/system/config`
+- **认证**: 无需认证
+- **请求参数**: 无
+- **响应数据**:
+```json
+{
+ "code": 200,
+ "message": "操作成功",
+ "data": {
+ "site_name": "CarrotSkin",
+ "site_description": "A Minecraft Skin Station",
+ "registration_enabled": true,
+ "max_textures_per_user": 100,
+ "max_profiles_per_user": 5
+ }
+}
+```
+
+## CustomSkin API
+
+### 1. 获取玩家信息
+
+- **URL**: `GET /api/v1/csl/{username}`
+- **认证**: 无需认证
+- **请求参数**:
+ - 路径参数: `username` - 玩家用户名
+- **响应数据**:
+```json
+{
+ "username": "PlayerName",
+ "textures": {
+ "default": "skin_hash_here",
+ "slim": "skin_hash_here",
+ "cape": "cape_hash_here",
+ "elytra": "cape_hash_here"
+ }
+}
+```
+或简化格式:
+```json
+{
+ "username": "PlayerName",
+ "skin": "skin_hash_here"
+}
+```
+
+### 2. 获取资源文件
+
+- **URL**: `GET /api/v1/csl/textures/{hash}`
+- **认证**: 无需认证
+- **请求参数**:
+ - 路径参数: `hash` - 资源哈希值
+- **响应数据**: 二进制文件内容
+
+## 健康检查
+
+### 1. 健康检查
+
+- **URL**: `GET /health`
+- **认证**: 无需认证
+- **请求参数**: 无
+- **响应数据**:
+```json
+{
+ "status": "ok"
+}
+```
+
+## 错误码说明
+
+| 错误码 | 说明 |
+|--------|------|
+| 200 | 操作成功 |
+| 400 | 请求参数错误 |
+| 401 | 未认证或认证失败 |
+| 403 | 无权限操作 |
+| 404 | 资源不存在 |
+| 500 | 服务器内部错误 |
+
+## 认证说明
+
+需要JWT认证的API需要在请求头中添加:
+
+```
+Authorization: Bearer {jwt_token}
+```
+
+JWT Token在用户登录或注册成功后返回,有效期内可用于访问需要认证的API。
diff --git a/README.md b/README.md
index e215bc4..8269615 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,125 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+# 🥕 CarrotSkin
-## Getting Started
+新一代现代化Minecraft Yggdrasil皮肤站,为创作者打造的专业皮肤管理平台。
-First, run the development server:
+## ✨ 项目特色
+### 🎨 现代化设计
+- **清新橘色主题**:温暖的橙色渐变配色方案,营造舒适的视觉体验
+- **玻璃态效果**:使用backdrop-filter实现现代化的毛玻璃效果
+- **流畅动画**:基于Framer Motion的丝滑动画和交互效果
+- **响应式布局**:完美适配桌面端、平板和移动设备
+
+### 🔧 技术栈
+- **Next.js 16**:最新的React框架,支持App Router
+- **TypeScript**:类型安全的开发体验
+- **Tailwind CSS v4**:现代化的CSS框架,支持自定义主题
+- **Framer Motion**:专业级动画库
+- **Heroicons**:精美的图标系统
+
+### 🚀 核心功能
+
+#### Yggdrasil API支持
+- 完整的Minecraft Yggdrasil认证系统
+- 安全可靠的API接口
+- 支持第三方启动器集成
+
+#### 皮肤管理
+- 无限皮肤存储空间
+- 3D皮肤预览功能
+- 多角色管理系统
+- 皮肤版本控制
+
+#### 社区功能
+- 皮肤分享和发现
+- 用户关注和互动
+- 皮肤收藏和点赞
+- 评论和评价系统
+
+#### 现代化体验
+- 拖拽上传皮肤
+- 实时预览效果
+- 批量管理操作
+- 智能搜索筛选
+
+## 🎯 页面结构
+
+### 主页 (/)
+- 现代化英雄区域展示
+- Yggdrasil特色功能介绍
+- 统计数据展示
+- 行动召唤区域
+
+### 皮肤库 (/skins)
+- 网格化皮肤展示
+- 高级搜索和筛选
+- 分类标签系统
+- 排序和分页功能
+
+### 个人中心 (/profile)
+- 用户信息管理
+- 角色和皮肤管理
+- 账户设置
+- API密钥管理
+
+## 🛠️ 开发环境
+
+### 安装依赖
```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
+npm install
```
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+### 启动开发服务器
+```bash
+npm run dev
+```
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+### 构建生产版本
+```bash
+npm run build
+```
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+## 🎨 设计亮点
-## Learn More
+### 视觉层次
+- 使用不同深浅的橙色创建视觉层次
+- 渐变背景增强深度感
+- 阴影效果营造立体感
-To learn more about Next.js, take a look at the following resources:
+### 交互体验
+- 鼠标跟随效果
+- 滚动视差动画
+- 悬停状态反馈
+- 加载动画效果
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+### 现代化元素
+- 圆角卡片设计
+- 渐变按钮效果
+- 玻璃态导航栏
+- 响应式断点优化
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+## 🔮 未来规划
-## Deploy on Vercel
+### 即将推出
+- 3D皮肤编辑器
+- 皮肤模板系统
+- 高级统计分析
+- 移动端APP
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+### 长期目标
+- AI皮肤生成
+- 皮肤交易市场
+- 创作者激励计划
+- 国际化支持
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+## 🤝 贡献指南
+
+欢迎提交Issue和Pull Request来帮助我们改进项目!
+
+## 📄 许可证
+
+MIT License - 详见 [LICENSE](LICENSE) 文件
+
+---
+
+**CarrotSkin** - 让Minecraft皮肤管理变得简单而优雅 🥕
diff --git a/package-lock.json b/package-lock.json
index 563edfd..744ec00 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,20 @@
"name": "carrotskin",
"version": "0.1.0",
"dependencies": {
+ "@auth/prisma-adapter": "^2.11.1",
+ "@headlessui/react": "^2.2.9",
+ "@heroicons/react": "^2.2.0",
+ "@prisma/client": "^7.1.0",
+ "@types/three": "^0.181.0",
+ "framer-motion": "^12.23.25",
+ "lucide-react": "^0.555.0",
"next": "16.0.7",
+ "next-auth": "^4.24.13",
+ "prisma": "^7.1.0",
"react": "19.2.0",
- "react-dom": "19.2.0"
+ "react-dom": "19.2.0",
+ "skinview3d": "^3.4.1",
+ "three": "^0.181.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -36,6 +47,139 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@auth/core": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmmirror.com/@auth/core/-/core-0.34.3.tgz",
+ "integrity": "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "@panva/hkdf": "^1.1.1",
+ "@types/cookie": "0.6.0",
+ "cookie": "0.6.0",
+ "jose": "^5.1.3",
+ "oauth4webapi": "^2.10.4",
+ "preact": "10.11.3",
+ "preact-render-to-string": "5.2.3"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "nodemailer": "^7"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@auth/core/node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@auth/core/node_modules/jose": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmmirror.com/jose/-/jose-5.10.0.tgz",
+ "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@auth/core/node_modules/preact-render-to-string": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmmirror.com/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
+ "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "pretty-format": "^3.8.0"
+ },
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
+ "node_modules/@auth/prisma-adapter": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmmirror.com/@auth/prisma-adapter/-/prisma-adapter-2.11.1.tgz",
+ "integrity": "sha512-Ke7DXP0Fy0Mlmjz/ZJLXwQash2UkA4621xCM0rMtEczr1kppLc/njCbUkHkIQ/PnmILjqSPEKeTjDPsYruvkug==",
+ "license": "ISC",
+ "dependencies": {
+ "@auth/core": "0.41.1"
+ },
+ "peerDependencies": {
+ "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6"
+ }
+ },
+ "node_modules/@auth/prisma-adapter/node_modules/@auth/core": {
+ "version": "0.41.1",
+ "resolved": "https://registry.npmmirror.com/@auth/core/-/core-0.41.1.tgz",
+ "integrity": "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==",
+ "license": "ISC",
+ "dependencies": {
+ "@panva/hkdf": "^1.2.1",
+ "jose": "^6.0.6",
+ "oauth4webapi": "^3.3.0",
+ "preact": "10.24.3",
+ "preact-render-to-string": "6.5.11"
+ },
+ "peerDependencies": {
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.2",
+ "nodemailer": "^7.0.7"
+ },
+ "peerDependenciesMeta": {
+ "@simplewebauthn/browser": {
+ "optional": true
+ },
+ "@simplewebauthn/server": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@auth/prisma-adapter/node_modules/jose": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmmirror.com/jose/-/jose-6.1.3.tgz",
+ "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@auth/prisma-adapter/node_modules/oauth4webapi": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmmirror.com/oauth4webapi/-/oauth4webapi-3.8.3.tgz",
+ "integrity": "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@auth/prisma-adapter/node_modules/preact-render-to-string": {
+ "version": "6.5.11",
+ "resolved": "https://registry.npmmirror.com/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
+ "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -229,6 +373,15 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz",
@@ -277,6 +430,73 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@chevrotain/cst-dts-gen": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz",
+ "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/gast": "10.5.0",
+ "@chevrotain/types": "10.5.0",
+ "lodash": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/gast": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmmirror.com/@chevrotain/gast/-/gast-10.5.0.tgz",
+ "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/types": "10.5.0",
+ "lodash": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-10.5.0.tgz",
+ "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/utils": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmmirror.com/@chevrotain/utils/-/utils-10.5.0.tgz",
+ "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@electric-sql/pglite": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmmirror.com/@electric-sql/pglite/-/pglite-0.3.2.tgz",
+ "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==",
+ "license": "Apache-2.0",
+ "peer": true
+ },
+ "node_modules/@electric-sql/pglite-socket": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmmirror.com/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz",
+ "integrity": "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "pglite-server": "dist/scripts/server.js"
+ },
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.3.2"
+ }
+ },
+ "node_modules/@electric-sql/pglite-tools": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmmirror.com/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz",
+ "integrity": "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@electric-sql/pglite": "0.3.2"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.7.1.tgz",
@@ -454,6 +674,100 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.26.28",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/react/-/react-0.26.28.tgz",
+ "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.2",
+ "@floating-ui/utils": "^0.2.8",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "node_modules/@headlessui/react": {
+ "version": "2.2.9",
+ "resolved": "https://registry.npmmirror.com/@headlessui/react/-/react-2.2.9.tgz",
+ "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react": "^0.26.16",
+ "@react-aria/focus": "^3.20.2",
+ "@react-aria/interactions": "^3.25.0",
+ "@tanstack/react-virtual": "^3.13.9",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@heroicons/react": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/@heroicons/react/-/react-2.2.0.tgz",
+ "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">= 16 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.6.tgz",
+ "integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
@@ -1022,6 +1336,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mrleebo/prisma-ast": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmmirror.com/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz",
+ "integrity": "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==",
+ "license": "MIT",
+ "dependencies": {
+ "chevrotain": "^10.5.0",
+ "lilconfig": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -1227,6 +1554,266 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@panva/hkdf": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/@panva/hkdf/-/hkdf-1.2.1.tgz",
+ "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/@prisma/client": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-7.1.0.tgz",
+ "integrity": "sha512-qf7GPYHmS/xybNiSOpzv9wBo+UwqfL2PeyX+08v+KVHDI0AlSCQIh5bBySkH3alu06NX9wy98JEnckhMHoMFfA==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@prisma/client-runtime-utils": "7.1.0"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0"
+ },
+ "peerDependencies": {
+ "prisma": "*",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/client-runtime-utils": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/client-runtime-utils/-/client-runtime-utils-7.1.0.tgz",
+ "integrity": "sha512-39xmeBrNTN40FzF34aJMjfX1PowVCqoT3UKUWBBSP3aXV05NRqGBC3x2wCDs96ti6ZgdiVzqnRDHtbzU8X+lPQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/config": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/config/-/config-7.1.0.tgz",
+ "integrity": "sha512-Uz+I43Wn1RYNHtuYtOhOnUcNMWp2Pd3GUDDKs37xlHptCGpzEG3MRR9L+8Y2ISMsMI24z/Ni+ww6OB/OO8M0sQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "c12": "3.1.0",
+ "deepmerge-ts": "7.1.5",
+ "effect": "3.18.4",
+ "empathic": "2.0.0"
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-7.1.0.tgz",
+ "integrity": "sha512-pPAckG6etgAsEBusmZiFwM9bldLSNkn++YuC4jCTJACdK5hLOVnOzX7eSL2FgaU6Gomd6wIw21snUX2dYroMZQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/dev": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/dev/-/dev-0.15.0.tgz",
+ "integrity": "sha512-KhWaipnFlS/fWEs6I6Oqjcy2S08vKGmxJ5LexqUl/3Ve0EgLUsZwdKF0MvqPM5F5ttw8GtfZarjM5y7VLwv9Ow==",
+ "license": "ISC",
+ "dependencies": {
+ "@electric-sql/pglite": "0.3.2",
+ "@electric-sql/pglite-socket": "0.0.6",
+ "@electric-sql/pglite-tools": "0.2.7",
+ "@hono/node-server": "1.19.6",
+ "@mrleebo/prisma-ast": "0.12.1",
+ "@prisma/get-platform": "6.8.2",
+ "@prisma/query-plan-executor": "6.18.0",
+ "foreground-child": "3.3.1",
+ "get-port-please": "3.1.2",
+ "hono": "4.10.6",
+ "http-status-codes": "2.3.0",
+ "pathe": "2.0.3",
+ "proper-lockfile": "4.1.2",
+ "remeda": "2.21.3",
+ "std-env": "3.9.0",
+ "valibot": "1.2.0",
+ "zeptomatch": "2.0.2"
+ }
+ },
+ "node_modules/@prisma/engines": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/engines/-/engines-7.1.0.tgz",
+ "integrity": "sha512-KQlraOybdHAzVv45KWKJzpR9mJLkib7/TyApQpqrsL7FUHfgjIcy8jrVGt3iNfG6/GDDl+LNlJ84JSQwIfdzxA==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.1.0",
+ "@prisma/engines-version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
+ "@prisma/fetch-engine": "7.1.0",
+ "@prisma/get-platform": "7.1.0"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
+ "resolved": "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba.tgz",
+ "integrity": "sha512-qZUevUh+yPhGT28rDQnV8V2kLnFjirzhVD67elRPIJHRsUV/mkII10HSrJrhK/U2GYgAxXR2VEREtq7AsfS8qw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.1.0.tgz",
+ "integrity": "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.1.0"
+ }
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-7.1.0.tgz",
+ "integrity": "sha512-GZYF5Q8kweXWGfn87hTu17kw7x1DgnehgKoE4Zg1BmHYF3y1Uu0QRY/qtSE4veH3g+LW8f9HKqA0tARG66bxxQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.1.0",
+ "@prisma/engines-version": "7.1.0-6.ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
+ "@prisma/get-platform": "7.1.0"
+ }
+ },
+ "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.1.0.tgz",
+ "integrity": "sha512-lq8hMdjKiZftuT5SssYB3EtQj8+YjL24/ZTLflQqzFquArKxBcyp6Xrblto+4lzIKJqnpOjfMiBjMvl7YuD7+Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "7.1.0"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.8.2",
+ "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-6.8.2.tgz",
+ "integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.8.2"
+ }
+ },
+ "node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
+ "version": "6.8.2",
+ "resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-6.8.2.tgz",
+ "integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/query-plan-executor": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmmirror.com/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz",
+ "integrity": "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/studio-core": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmmirror.com/@prisma/studio-core/-/studio-core-0.8.2.tgz",
+ "integrity": "sha512-/iAEWEUpTja+7gVMu1LtR2pPlvDmveAwMHdTWbDeGlT7yiv0ZTCPpmeAGdq/Y9aJ9Zj1cEGBXGRbmmNPj022PQ==",
+ "license": "UNLICENSED",
+ "peerDependencies": {
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/focus": {
+ "version": "3.21.2",
+ "resolved": "https://registry.npmmirror.com/@react-aria/focus/-/focus-3.21.2.tgz",
+ "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/interactions": "^3.25.6",
+ "@react-aria/utils": "^3.31.0",
+ "@react-types/shared": "^3.32.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+ }
+ },
+ "node_modules/@react-aria/interactions": {
+ "version": "3.25.6",
+ "resolved": "https://registry.npmmirror.com/@react-aria/interactions/-/interactions-3.25.6.tgz",
+ "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.10",
+ "@react-aria/utils": "^3.31.0",
+ "@react-stately/flags": "^3.1.2",
+ "@react-types/shared": "^3.32.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+ }
+ },
+ "node_modules/@react-aria/ssr": {
+ "version": "3.9.10",
+ "resolved": "https://registry.npmmirror.com/@react-aria/ssr/-/ssr-3.9.10.tgz",
+ "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+ }
+ },
+ "node_modules/@react-aria/utils": {
+ "version": "3.31.0",
+ "resolved": "https://registry.npmmirror.com/@react-aria/utils/-/utils-3.31.0.tgz",
+ "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.10",
+ "@react-stately/flags": "^3.1.2",
+ "@react-stately/utils": "^3.10.8",
+ "@react-types/shared": "^3.32.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+ }
+ },
+ "node_modules/@react-stately/flags": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/@react-stately/flags/-/flags-3.1.2.tgz",
+ "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
+ "node_modules/@react-stately/utils": {
+ "version": "3.10.8",
+ "resolved": "https://registry.npmmirror.com/@react-stately/utils/-/utils-3.10.8.tgz",
+ "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+ }
+ },
+ "node_modules/@react-types/shared": {
+ "version": "3.32.1",
+ "resolved": "https://registry.npmmirror.com/@react-types/shared/-/shared-3.32.1.tgz",
+ "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1234,6 +1821,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1514,6 +2107,39 @@
"tailwindcss": "4.1.17"
}
},
+ "node_modules/@tanstack/react-virtual": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmmirror.com/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
+ "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.13.12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmmirror.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
+ "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -1525,6 +2151,13 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
@@ -1560,7 +2193,6 @@
"version": "19.2.7",
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
- "dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -1577,6 +2209,33 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.181.0",
+ "resolved": "https://registry.npmmirror.com/@types/three/-/three-0.181.0.tgz",
+ "integrity": "sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.22.0"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.1",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
@@ -2117,6 +2776,12 @@
"win32"
]
},
+ "node_modules/@webgpu/types": {
+ "version": "0.1.67",
+ "resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.67.tgz",
+ "integrity": "sha512-uk53+2ECGUkWoDFez/hymwpRfdgdIn6y1ref70fEecGMe5607f4sozNFgBk0oxlr7j2CRGWBEc3IBYMmFdGGTQ==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
@@ -2384,6 +3049,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/axe-core": {
"version": "4.11.0",
"resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.11.0.tgz",
@@ -2480,6 +3154,34 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/c12": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/c12/-/c12-3.1.0.tgz",
+ "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.3",
+ "confbox": "^0.2.2",
+ "defu": "^6.1.4",
+ "dotenv": "^16.6.1",
+ "exsolve": "^1.0.7",
+ "giget": "^2.0.0",
+ "jiti": "^2.4.2",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^1.0.0",
+ "pkg-types": "^2.2.0",
+ "rc9": "^2.1.2"
+ },
+ "peerDependencies": {
+ "magicast": "^0.3.5"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
@@ -2577,12 +3279,59 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chevrotain": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-10.5.0.tgz",
+ "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/cst-dts-gen": "10.5.0",
+ "@chevrotain/gast": "10.5.0",
+ "@chevrotain/types": "10.5.0",
+ "@chevrotain/utils": "10.5.0",
+ "lodash": "4.17.21",
+ "regexp-to-ast": "0.5.0"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/citty": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz",
+ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+ "license": "MIT",
+ "dependencies": {
+ "consola": "^3.2.3"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -2610,6 +3359,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/confbox": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz",
+ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+ "license": "MIT"
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2617,11 +3381,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -2636,7 +3408,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -2725,6 +3496,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/deepmerge-ts": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+ "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -2761,6 +3541,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/defu": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz",
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "license": "MIT"
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2784,6 +3585,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2799,6 +3612,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/effect": {
+ "version": "3.18.4",
+ "resolved": "https://registry.npmmirror.com/effect/-/effect-3.18.4.tgz",
+ "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "fast-check": "^3.23.1"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.264",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.264.tgz",
@@ -2813,6 +3636,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/empathic": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/empathic/-/empathic-2.0.0.tgz",
+ "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@@ -3453,6 +4285,34 @@
"node": ">=0.10.0"
}
},
+ "node_modules/exsolve": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+ "license": "MIT"
+ },
+ "node_modules/fast-check": {
+ "version": "3.23.2",
+ "resolved": "https://registry.npmmirror.com/fast-check/-/fast-check-3.23.2.tgz",
+ "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "pure-rand": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3514,6 +4374,12 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3594,6 +4460,49 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/framer-motion": {
+ "version": "12.23.25",
+ "resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.23.25.tgz",
+ "integrity": "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.23",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
@@ -3635,6 +4544,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz",
@@ -3680,6 +4598,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-port-please": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/get-port-please/-/get-port-please-3.1.2.tgz",
+ "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==",
+ "license": "MIT"
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
@@ -3725,6 +4649,23 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/giget": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/giget/-/giget-2.0.0.tgz",
+ "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.0",
+ "defu": "^6.1.4",
+ "node-fetch-native": "^1.6.6",
+ "nypm": "^0.6.0",
+ "pathe": "^2.0.3"
+ },
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -3785,9 +4726,14 @@
"version": "4.2.11",
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
"license": "ISC"
},
+ "node_modules/grammex": {
+ "version": "3.1.12",
+ "resolved": "https://registry.npmmirror.com/grammex/-/grammex-3.1.12.tgz",
+ "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
+ "license": "MIT"
+ },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
@@ -3906,6 +4852,38 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/hono": {
+ "version": "4.10.6",
+ "resolved": "https://registry.npmmirror.com/hono/-/hono-4.10.6.tgz",
+ "integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/http-status-codes": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/http-status-codes/-/http-status-codes-2.3.0.tgz",
+ "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
+ "license": "MIT"
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz",
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
@@ -4228,6 +5206,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
@@ -4384,7 +5368,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
"license": "ISC"
},
"node_modules/iterator.prototype": {
@@ -4409,12 +5392,20 @@
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "4.15.9",
+ "resolved": "https://registry.npmmirror.com/jose/-/jose-4.15.9.tgz",
+ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4803,6 +5794,15 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
@@ -4819,6 +5819,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4826,6 +5832,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4849,6 +5861,30 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lru.min": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.3.tgz",
+ "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.555.0",
+ "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.555.0.tgz",
+ "integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -4879,6 +5915,12 @@
"node": ">= 8"
}
},
+ "node_modules/meshoptimizer": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
+ "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
+ "license": "MIT"
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
@@ -4916,6 +5958,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.23.23",
+ "resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.23.23.tgz",
+ "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
@@ -4923,6 +5980,47 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/mysql2": {
+ "version": "3.15.3",
+ "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.3.tgz",
+ "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.1",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.0",
+ "long": "^5.2.1",
+ "lru.min": "^1.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz",
+ "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
+ "license": "MIT",
+ "dependencies": {
+ "lru-cache": "^7.14.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/named-placeholders/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
@@ -5016,6 +6114,38 @@
}
}
},
+ "node_modules/next-auth": {
+ "version": "4.24.13",
+ "resolved": "https://registry.npmmirror.com/next-auth/-/next-auth-4.24.13.tgz",
+ "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@panva/hkdf": "^1.0.2",
+ "cookie": "^0.7.0",
+ "jose": "^4.15.5",
+ "oauth": "^0.9.15",
+ "openid-client": "^5.4.0",
+ "preact": "^10.6.3",
+ "preact-render-to-string": "^5.1.19",
+ "uuid": "^8.3.2"
+ },
+ "peerDependencies": {
+ "@auth/core": "0.34.3",
+ "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16",
+ "nodemailer": "^7.0.7",
+ "react": "^17.0.2 || ^18 || ^19",
+ "react-dom": "^17.0.2 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@auth/core": {
+ "optional": true
+ },
+ "nodemailer": {
+ "optional": true
+ }
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz",
@@ -5044,6 +6174,12 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node-fetch-native": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+ "license": "MIT"
+ },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz",
@@ -5051,6 +6187,41 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nypm": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.6.2.tgz",
+ "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.2",
+ "pathe": "^2.0.3",
+ "pkg-types": "^2.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "bin": {
+ "nypm": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": "^14.16.0 || >=16.10.0"
+ }
+ },
+ "node_modules/oauth": {
+ "version": "0.9.15",
+ "resolved": "https://registry.npmmirror.com/oauth/-/oauth-0.9.15.tgz",
+ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
+ "license": "MIT"
+ },
+ "node_modules/oauth4webapi": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmmirror.com/oauth4webapi/-/oauth4webapi-2.17.0.tgz",
+ "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==",
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
@@ -5061,6 +6232,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -5174,6 +6354,54 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "license": "MIT"
+ },
+ "node_modules/oidc-token-hash": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
+ "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
+ "node_modules/openid-client": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmmirror.com/openid-client/-/openid-client-5.7.1.tgz",
+ "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
+ "license": "MIT",
+ "dependencies": {
+ "jose": "^4.15.9",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/openid-client/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/openid-client/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
@@ -5269,7 +6497,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5282,6 +6509,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@@ -5301,6 +6540,17 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pkg-types": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz",
+ "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.2.2",
+ "exsolve": "^1.0.7",
+ "pathe": "^2.0.3"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -5340,6 +6590,42 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postgres": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmmirror.com/postgres/-/postgres-3.4.7.tgz",
+ "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/porsager"
+ }
+ },
+ "node_modules/preact": {
+ "version": "10.24.3",
+ "resolved": "https://registry.npmmirror.com/preact/-/preact-10.24.3.tgz",
+ "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/preact-render-to-string": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmmirror.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
+ "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
+ "license": "MIT",
+ "dependencies": {
+ "pretty-format": "^3.8.0"
+ },
+ "peerDependencies": {
+ "preact": ">=10"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5350,6 +6636,46 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/pretty-format": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-3.8.0.tgz",
+ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
+ "license": "MIT"
+ },
+ "node_modules/prisma": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/prisma/-/prisma-7.1.0.tgz",
+ "integrity": "sha512-dy/3urE4JjhdiW5b09pGjVhGI7kPESK2VlCDrCqeYK5m5SslAtG5FCGnZWP7E8Sdg+Ow1wV2mhJH5RTFL5gEsw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@prisma/config": "7.1.0",
+ "@prisma/dev": "0.15.0",
+ "@prisma/engines": "7.1.0",
+ "@prisma/studio-core": "0.8.2",
+ "mysql2": "3.15.3",
+ "postgres": "3.4.7"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24.0"
+ },
+ "peerDependencies": {
+ "better-sqlite3": ">=9.0.0",
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "better-sqlite3": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz",
@@ -5362,6 +6688,23 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/proper-lockfile/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
@@ -5372,6 +6715,22 @@
"node": ">=6"
}
},
+ "node_modules/pure-rand": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-6.1.0.tgz",
+ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5393,6 +6752,16 @@
],
"license": "MIT"
},
+ "node_modules/rc9": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz",
+ "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.4",
+ "destr": "^2.0.3"
+ }
+ },
"node_modules/react": {
"version": "19.2.0",
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz",
@@ -5423,6 +6792,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5446,6 +6828,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/regexp-to-ast": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmmirror.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz",
+ "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==",
+ "license": "MIT"
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -5467,6 +6855,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/remeda": {
+ "version": "2.21.3",
+ "resolved": "https://registry.npmmirror.com/remeda/-/remeda-2.21.3.tgz",
+ "integrity": "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^4.39.1"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
@@ -5508,6 +6905,15 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
@@ -5598,6 +7004,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz",
@@ -5614,6 +7026,11 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -5725,7 +7142,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -5738,7 +7154,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5820,6 +7235,65 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/skinview-utils": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmmirror.com/skinview-utils/-/skinview-utils-0.7.1.tgz",
+ "integrity": "sha512-4eLrMqR526ehlZbsd8SuZ/CHpS9GiH0xUMoV+PYlJVi95ZFz5HJu7Spt5XYa72DRS7wgt5qquvHZf0XZJgmu9Q==",
+ "license": "MIT"
+ },
+ "node_modules/skinview3d": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmmirror.com/skinview3d/-/skinview3d-3.4.1.tgz",
+ "integrity": "sha512-WVN1selfDSAoQB7msLs3ueJjW/pge3nsmbqxJeXPnN/qIJ1GJKpMZO8mavSvMojaMrmpSgOJWfYUkK9B34ts2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "^0.156.0",
+ "skinview-utils": "^0.7.1",
+ "three": "^0.156.0"
+ }
+ },
+ "node_modules/skinview3d/node_modules/@types/three": {
+ "version": "0.156.0",
+ "resolved": "https://registry.npmmirror.com/@types/three/-/three-0.156.0.tgz",
+ "integrity": "sha512-733bXDSRdlrxqOmQuOmfC1UBRuJ2pREPk8sWnx9MtIJEVDQMx8U0NQO5MVVaOrjzDPyLI+cFPim2X/ss9v0+LQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "fflate": "~0.6.10",
+ "meshoptimizer": "~0.18.1"
+ }
+ },
+ "node_modules/skinview3d/node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
+ "license": "MIT"
+ },
+ "node_modules/skinview3d/node_modules/meshoptimizer": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+ "license": "MIT"
+ },
+ "node_modules/skinview3d/node_modules/three": {
+ "version": "0.156.1",
+ "resolved": "https://registry.npmmirror.com/three/-/three-0.156.1.tgz",
+ "integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ==",
+ "license": "MIT"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5829,6 +7303,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -5836,6 +7319,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/std-env": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz",
+ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+ "license": "MIT"
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -6035,6 +7524,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tabbable": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmmirror.com/tabbable/-/tabbable-6.3.0.tgz",
+ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
+ "license": "MIT"
+ },
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.17.tgz",
@@ -6056,6 +7551,21 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/three": {
+ "version": "0.181.2",
+ "resolved": "https://registry.npmmirror.com/three/-/three-0.181.2.tgz",
+ "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==",
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz",
+ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -6176,6 +7686,18 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -6258,7 +7780,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
@@ -6395,11 +7917,42 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/valibot": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/valibot/-/valibot-1.2.0.tgz",
+ "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -6530,6 +8083,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zeptomatch": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/zeptomatch/-/zeptomatch-2.0.2.tgz",
+ "integrity": "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==",
+ "license": "MIT",
+ "dependencies": {
+ "grammex": "^3.1.10"
+ }
+ },
"node_modules/zod": {
"version": "4.1.13",
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.1.13.tgz",
diff --git a/package.json b/package.json
index f70bb43..b6e4e62 100644
--- a/package.json
+++ b/package.json
@@ -9,9 +9,20 @@
"lint": "eslint"
},
"dependencies": {
+ "@auth/prisma-adapter": "^2.11.1",
+ "@headlessui/react": "^2.2.9",
+ "@heroicons/react": "^2.2.0",
+ "@prisma/client": "^7.1.0",
+ "@types/three": "^0.181.0",
+ "framer-motion": "^12.23.25",
+ "lucide-react": "^0.555.0",
"next": "16.0.7",
+ "next-auth": "^4.24.13",
+ "prisma": "^7.1.0",
"react": "19.2.0",
- "react-dom": "19.2.0"
+ "react-dom": "19.2.0",
+ "skinview3d": "^3.4.1",
+ "three": "^0.181.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
diff --git a/postcss.config.mjs b/postcss.config.mjs
index 61e3684..a7f73a2 100644
--- a/postcss.config.mjs
+++ b/postcss.config.mjs
@@ -1,7 +1,5 @@
-const config = {
+export default {
plugins: {
- "@tailwindcss/postcss": {},
+ '@tailwindcss/postcss': {},
},
-};
-
-export default config;
+}
diff --git a/prisma.config.ts b/prisma.config.ts
new file mode 100644
index 0000000..9c5e959
--- /dev/null
+++ b/prisma.config.ts
@@ -0,0 +1,14 @@
+// This file was generated by Prisma and assumes you have installed the following:
+// npm install --save-dev prisma dotenv
+import "dotenv/config";
+import { defineConfig, env } from "prisma/config";
+
+export default defineConfig({
+ schema: "prisma/schema.prisma",
+ migrations: {
+ path: "prisma/migrations",
+ },
+ datasource: {
+ url: env("DATABASE_URL"),
+ },
+});
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..f51cc2c
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,14 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
+// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
+
+generator client {
+ provider = "prisma-client"
+ output = "../src/generated/prisma"
+}
+
+datasource db {
+ provider = "postgresql"
+}
diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx
new file mode 100644
index 0000000..0fe95bb
--- /dev/null
+++ b/src/app/auth/layout.tsx
@@ -0,0 +1,11 @@
+export default function AuthLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx
new file mode 100644
index 0000000..ca91280
--- /dev/null
+++ b/src/app/auth/page.tsx
@@ -0,0 +1,732 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { motion, AnimatePresence } from 'framer-motion';
+import { EyeIcon, EyeSlashIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
+import { useAuth } from '@/contexts/AuthContext';
+import { errorManager } from '@/components/ErrorNotification';
+
+export default function AuthPage() {
+ const [isLoginMode, setIsLoginMode] = useState(true);
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ password: '',
+ confirmPassword: '',
+ verificationCode: '',
+ rememberMe: false,
+ agreeToTerms: false
+ });
+
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [errors, setErrors] = useState>({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [authError, setAuthError] = useState('');
+ const [isSendingCode, setIsSendingCode] = useState(false);
+ const [codeTimer, setCodeTimer] = useState(0);
+
+ const { login, register } = useAuth();
+ const router = useRouter();
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+ if (codeTimer > 0) {
+ interval = setInterval(() => {
+ setCodeTimer(prev => prev - 1);
+ }, 1000);
+ }
+ return () => clearInterval(interval);
+ }, [codeTimer]);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value, type, checked } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value
+ }));
+
+ if (errors[name]) {
+ setErrors(prev => ({ ...prev, [name]: '' }));
+ }
+ if (authError) {
+ setAuthError('');
+ }
+ };
+
+ const validateLoginForm = () => {
+ const newErrors: Record = {};
+
+ if (!formData.username.trim()) {
+ newErrors.username = '请输入用户名或邮箱';
+ }
+
+ if (!formData.password) {
+ newErrors.password = '请输入密码';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const validateRegisterForm = () => {
+ const newErrors: Record = {};
+
+ if (!formData.username.trim()) {
+ newErrors.username = '用户名不能为空';
+ } else if (formData.username.length < 3) {
+ newErrors.username = '用户名至少需要3个字符';
+ } else if (formData.username.length > 50) {
+ newErrors.username = '用户名不能超过50个字符';
+ }
+
+ if (!formData.email.trim()) {
+ newErrors.email = '邮箱不能为空';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = '请输入有效的邮箱地址';
+ }
+
+ if (!formData.password) {
+ newErrors.password = '密码不能为空';
+ } else if (formData.password.length < 6) {
+ newErrors.password = '密码至少需要6个字符';
+ } else if (formData.password.length > 128) {
+ newErrors.password = '密码不能超过128个字符';
+ }
+
+ if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = '两次输入的密码不一致';
+ }
+
+ if (!formData.verificationCode) {
+ newErrors.verificationCode = '请输入验证码';
+ } else if (!/^\d{6}$/.test(formData.verificationCode)) {
+ newErrors.verificationCode = '验证码应为6位数字';
+ }
+
+ if (!formData.agreeToTerms) {
+ newErrors.agreeToTerms = '请同意服务条款';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const getPasswordStrength = () => {
+ const password = formData.password;
+ if (password.length === 0) return { strength: 0, label: '', color: 'bg-gray-200' };
+ if (password.length < 6) return { strength: 1, label: '弱', color: 'bg-red-500' };
+ if (password.length < 10) return { strength: 2, label: '中等', color: 'bg-yellow-500' };
+ if (password.length >= 15) return { strength: 4, label: '很强', color: 'bg-green-500' };
+ return { strength: 3, label: '强', color: 'bg-blue-500' };
+ };
+
+ const passwordStrength = getPasswordStrength();
+
+ const handleSendCode = async () => {
+ if (!formData.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ setErrors(prev => ({ ...prev, email: '请输入有效的邮箱地址' }));
+ return;
+ }
+
+ setIsSendingCode(true);
+ try {
+ const response = await fetch('/api/v1/auth/send-code', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ email: formData.email,
+ type: 'register'
+ }),
+ });
+
+ const data = await response.json();
+
+ if (data.code === 200) {
+ setCodeTimer(60);
+ errorManager.showSuccess('验证码已发送到您的邮箱');
+ } else {
+ setErrors(prev => ({ ...prev, email: data.message || '发送验证码失败' }));
+ errorManager.showError(data.message || '发送验证码失败');
+ }
+ } catch (error) {
+ setErrors(prev => ({ ...prev, email: '发送验证码失败,请稍后重试' }));
+ errorManager.showError('发送验证码失败,请稍后重试');
+ } finally {
+ setIsSendingCode(false);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (isLoginMode) {
+ if (!validateLoginForm()) return;
+
+ setIsLoading(true);
+ setAuthError('');
+
+ try {
+ await login(formData.username, formData.password);
+
+ if (formData.rememberMe) {
+ localStorage.setItem('rememberMe', 'true');
+ } else {
+ localStorage.removeItem('rememberMe');
+ }
+
+ errorManager.showSuccess('登录成功!');
+ router.push('/');
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '登录失败,请检查用户名和密码';
+ setAuthError(errorMessage);
+ errorManager.showError(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ } else {
+ if (!validateRegisterForm()) return;
+
+ setIsLoading(true);
+ setAuthError('');
+
+ try {
+ await register(formData.username, formData.email, formData.password, formData.verificationCode);
+ errorManager.showSuccess('注册成功!欢迎加入CarrotSkin!');
+ router.push('/');
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '注册失败,请稍后重试';
+ setAuthError(errorMessage);
+ errorManager.showError(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ const switchMode = () => {
+ setIsLoginMode(!isLoginMode);
+ setAuthError('');
+ setErrors({});
+ setFormData({
+ username: '',
+ email: '',
+ password: '',
+ confirmPassword: '',
+ verificationCode: '',
+ rememberMe: false,
+ agreeToTerms: false
+ });
+ };
+
+ return (
+
+ {/* Left Side - Orange Section */}
+
+
+
+ CS
+
+
+
+ {isLoginMode ? '欢迎回来' : '加入我们'}
+
+
+
+ {isLoginMode ? '继续你的创作之旅' : '开始你的创作之旅'}
+
+
+
+
+
+
+
+
+
+
+ 已有超过 10,000 位创作者加入我们
+
+
+
+
+
+ {/* Right Side - White Section */}
+
+
+ {/* Back to Home Button */}
+
+
+
+ 返回主页
+
+
+
+ {/* Header */}
+
+
+ {isLoginMode ? '登录账户' : '创建账户'}
+
+
+ {isLoginMode ? '登录您的CarrotSkin账户' : '加入我们,开始创作'}
+
+
+
+ {authError && (
+
+ {authError}
+
+ )}
+
+
+
+ {/* Social Login */}
+
+
+
+
+
+
+ GitHub
+
+
+
+
+ Microsoft
+
+
+
+
+ {/* Mode Switch */}
+
+
+ {isLoginMode ? '还没有账户?' : '已有账户?'}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..12290ab 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,15 +1,13 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@import "tailwindcss";
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
-}
-
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --navbar-height: 64px; /* 与pt-16对应 */
}
@media (prefers-color-scheme: dark) {
@@ -20,7 +18,90 @@
}
body {
- background: var(--background);
color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ background: var(--background);
+ font-family: 'Inter', Arial, Helvetica, sans-serif;
+}
+
+/* Custom utility classes */
+.text-balance {
+ text-wrap: balance;
+}
+
+/* Custom component classes */
+.btn-carrot {
+ background-color: #f97316;
+ color: white;
+ font-weight: 500;
+ padding: 0.5rem 1rem;
+ border-radius: 0.5rem;
+ transition: background-color 0.2s;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+}
+
+.btn-carrot:hover {
+ background-color: #ea580c;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+}
+
+.btn-carrot-outline {
+ border: 2px solid #f97316;
+ color: #f97316;
+ font-weight: 500;
+ padding: 0.5rem 1rem;
+ border-radius: 0.5rem;
+ transition: all 0.2s;
+}
+
+.btn-carrot-outline:hover {
+ background-color: #f97316;
+ color: white;
+}
+
+.card-minecraft {
+ background-color: white;
+ border: 2px solid #fed7aa;
+ border-radius: 0.5rem;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ transition: all 0.2s;
+}
+
+.card-minecraft:hover {
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+}
+
+@media (prefers-color-scheme: dark) {
+ .card-minecraft {
+ background-color: #1f2937;
+ border-color: #c2410c;
+ }
+}
+
+.text-gradient {
+ background: linear-gradient(to right, #fb923c, #f97316);
+ background-clip: text;
+ -webkit-background-clip: text;
+ color: transparent;
+}
+
+.bg-gradient-carrot {
+ background: linear-gradient(to bottom right, #fb923c, #f97316, #ea580c);
+}
+
+/* 现代布局解决方案 */
+@layer utilities {
+ /* 全屏减去navbar高度 */
+ .h-screen-nav {
+ height: calc(100vh - var(--navbar-height));
+ }
+
+ /* 侧栏最大高度,确保底部按钮可见 */
+ .sidebar-max-height {
+ max-height: calc(100vh - var(--navbar-height) - 120px);
+ }
+
+ /* 首页hero section专用高度 */
+ .min-h-screen-nav {
+ min-height: calc(100vh - var(--navbar-height));
+ }
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f7fa87e..41af848 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,20 +1,28 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Inter } from "next/font/google";
import "./globals.css";
+import Navbar from "@/components/Navbar";
+import { AuthProvider } from "@/contexts/AuthContext";
+import { MainContent } from "@/components/MainContent";
+import { ErrorNotificationContainer } from "@/components/ErrorNotification";
+import ScrollToTop from "@/components/ScrollToTop";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const inter = Inter({
subsets: ["latin"],
+ weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
+ display: 'swap',
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "CarrotSkin - 现代化Minecraft Yggdrasil皮肤站",
+ description: "新一代Minecraft Yggdrasil皮肤站,为创作者打造的现代化皮肤管理平台",
+ keywords: "Minecraft, 皮肤站, Yggdrasil, CarrotSkin, 我的世界, 皮肤管理",
+ authors: [{ name: "CarrotSkin Team" }],
+ openGraph: {
+ title: "CarrotSkin - 现代化Minecraft Yggdrasil皮肤站",
+ description: "新一代Minecraft Yggdrasil皮肤站,为创作者打造的现代化皮肤管理平台",
+ type: "website",
+ },
};
export default function RootLayout({
@@ -23,11 +31,14 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
- {children}
+
+
+
+
+ {children}
+
+
+
);
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..fff1627
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,108 @@
+'use client';
+
+import Link from 'next/link';
+import { motion } from 'framer-motion';
+import { HomeIcon, ArrowLeftIcon } from '@heroicons/react/24/outline';
+
+export default function NotFound() {
+ return (
+
+
+ {/* 404 数字 */}
+
+
+ 404
+
+
+
+
+ {/* 错误信息 */}
+
+
+ 页面不见了
+
+
+ 抱歉,我们找不到您要访问的页面。它可能已被移动、删除,或者您输入的链接不正确。
+
+
+
+ {/* Minecraft 风格的装饰 */}
+
+
+
+
+ {/* 操作按钮 */}
+
+
+
+ 返回主页
+
+
+
+
+
+ {/* 额外的帮助信息 */}
+
+ 如果问题持续存在,请
+
+ 联系我们
+
+ 的支持团队
+
+
+
+
+ {/* 背景装饰 */}
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 295f8fd..2a10a68 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,65 +1,304 @@
-import Image from "next/image";
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { motion, useScroll, useTransform } from 'framer-motion';
+import {
+ ArrowRightIcon,
+ ShieldCheckIcon,
+ CloudArrowUpIcon,
+ ShareIcon,
+ CubeIcon,
+ UserGroupIcon,
+ SparklesIcon,
+ RocketLaunchIcon
+} from '@heroicons/react/24/outline';
export default function Home() {
+ const { scrollYProgress } = useScroll();
+ const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0]);
+ const scale = useTransform(scrollYProgress, [0, 0.3], [1, 0.8]);
+
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+ const [isHovered, setIsHovered] = useState(false);
+
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ setMousePosition({ x: e.clientX, y: e.clientY });
+ };
+ window.addEventListener('mousemove', handleMouseMove);
+ return () => window.removeEventListener('mousemove', handleMouseMove);
+ }, []);
+
+ const features = [
+ {
+ icon: ShieldCheckIcon,
+ title: "Yggdrasil认证",
+ description: "完整的Minecraft Yggdrasil API支持,安全可靠的用户认证系统",
+ color: "from-amber-400 to-orange-500"
+ },
+ {
+ icon: CloudArrowUpIcon,
+ title: "云端存储",
+ description: "无限皮肤存储空间,自动备份,随时随地访问你的皮肤库",
+ color: "from-orange-400 to-red-500"
+ },
+ {
+ icon: ShareIcon,
+ title: "社区分享",
+ description: "与全球玩家分享创作,发现灵感,建立你的粉丝群体",
+ color: "from-red-400 to-pink-500"
+ },
+ {
+ icon: CubeIcon,
+ title: "3D预览",
+ description: "实时3D皮肤预览,360度旋转查看,支持多种渲染模式",
+ color: "from-pink-400 to-purple-500"
+ }
+ ];
+
+ const stats = [
+ { number: "50K+", label: "注册用户" },
+ { number: "200K+", label: "皮肤上传" },
+ { number: "1M+", label: "月活用户" },
+ { number: "99.9%", label: "服务可用性" }
+ ];
+
return (
-
-
-
+ {/* Animated Background */}
+
+
+
+
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
);
}
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
new file mode 100644
index 0000000..6ae314f
--- /dev/null
+++ b/src/app/profile/page.tsx
@@ -0,0 +1,1359 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ UserCircleIcon,
+ PencilIcon,
+ TrashIcon,
+ PlusIcon,
+ EyeIcon,
+ ArrowDownTrayIcon,
+ Cog6ToothIcon,
+ UserIcon,
+ PhotoIcon,
+ KeyIcon,
+ EnvelopeIcon,
+
+ HeartIcon,
+ ArrowLeftOnRectangleIcon,
+ CloudArrowUpIcon,
+ XMarkIcon,
+ ArrowPathIcon,
+ XCircleIcon
+} from '@heroicons/react/24/outline';
+import { useAuth } from '@/contexts/AuthContext';
+import {
+ getMyTextures,
+ getFavoriteTextures,
+ toggleFavorite,
+ getProfiles,
+ createProfile,
+ updateProfile,
+ deleteProfile,
+ setActiveProfile,
+ getUserProfile,
+ updateUserProfile,
+ uploadTexture,
+ type Texture,
+ type Profile
+} from '@/lib/api';
+
+interface UserProfile {
+ id: number;
+ username: string;
+ email: string;
+ avatar?: string;
+ points: number;
+ role: string;
+ status: number;
+ last_login_at?: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export default function ProfilePage() {
+ const [activeTab, setActiveTab] = useState<'characters' | 'skins' | 'favorites' | 'settings'>('characters');
+ const [profiles, setProfiles] = useState
([]);
+ const [mySkins, setMySkins] = useState([]);
+ const [favoriteSkins, setFavoriteSkins] = useState([]);
+ const [userProfile, setUserProfile] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isUploading, setIsUploading] = useState(false);
+ const [showCreateCharacter, setShowCreateCharacter] = useState(false);
+ const [showUploadSkin, setShowUploadSkin] = useState(false);
+ const [newCharacterName, setNewCharacterName] = useState('');
+ const [newSkinData, setNewSkinData] = useState({
+ name: '',
+ description: '',
+ type: 'SKIN' as 'SKIN' | 'CAPE',
+ is_public: false,
+ is_slim: false
+ });
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [editingProfile, setEditingProfile] = useState(null);
+ const [editProfileName, setEditProfileName] = useState('');
+ const [uploadProgress, setUploadProgress] = useState(0);
+ const [avatarFile, setAvatarFile] = useState(null);
+ const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
+ const [avatarUploadProgress, setAvatarUploadProgress] = useState(0);
+ const [error, setError] = useState(null);
+
+ const { user, isAuthenticated, logout } = useAuth();
+
+ // 加载用户数据
+ useEffect(() => {
+ if (isAuthenticated) {
+ loadUserData();
+ }
+ }, [isAuthenticated]);
+
+ const loadUserData = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ // 加载用户信息
+ const userResponse = await getUserProfile();
+ if (userResponse.code === 200) {
+ setUserProfile(userResponse.data);
+ } else {
+ throw new Error(userResponse.message || '获取用户信息失败');
+ }
+
+ // 加载用户档案
+ const profilesResponse = await getProfiles();
+ if (profilesResponse.code === 200) {
+ setProfiles(profilesResponse.data);
+ } else {
+ throw new Error(profilesResponse.message || '获取角色列表失败');
+ }
+
+ // 加载用户皮肤
+ const mySkinsResponse = await getMyTextures({ page: 1, page_size: 50 });
+ if (mySkinsResponse.code === 200) {
+ setMySkins(mySkinsResponse.data.list || []);
+ } else {
+ throw new Error(mySkinsResponse.message || '获取皮肤列表失败');
+ }
+
+ // 加载收藏的皮肤
+ const favoritesResponse = await getFavoriteTextures({ page: 1, page_size: 50 });
+ if (favoritesResponse.code === 200) {
+ setFavoriteSkins(favoritesResponse.data.list || []);
+ } else {
+ throw new Error(favoritesResponse.message || '获取收藏列表失败');
+ }
+ } catch (error) {
+ console.error('加载用户数据失败:', error);
+ setError(error instanceof Error ? error.message : '加载数据失败,请稍后重试');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleCreateCharacter = async () => {
+ if (!newCharacterName.trim()) {
+ alert('请输入角色名称');
+ return;
+ }
+
+ try {
+ const response = await createProfile(newCharacterName.trim());
+ if (response.code === 200) {
+ setProfiles(prev => [...prev, response.data]);
+ setNewCharacterName('');
+ setShowCreateCharacter(false);
+ alert('角色创建成功!');
+ } else {
+ throw new Error(response.message || '创建角色失败');
+ }
+ } catch (error) {
+ console.error('创建角色失败:', error);
+ alert(error instanceof Error ? error.message : '创建角色失败,请稍后重试');
+ }
+ };
+
+ const handleDeleteCharacter = async (uuid: string) => {
+ const character = profiles.find(p => p.uuid === uuid);
+ if (!character) return;
+
+ if (!confirm(`确定要删除角色 "${character.name}" 吗?此操作不可恢复。`)) return;
+
+ try {
+ const response = await deleteProfile(uuid);
+ if (response.code === 200) {
+ setProfiles(prev => prev.filter(profile => profile.uuid !== uuid));
+ alert('角色删除成功!');
+ } else {
+ throw new Error(response.message || '删除角色失败');
+ }
+ } catch (error) {
+ console.error('删除角色失败:', error);
+ alert(error instanceof Error ? error.message : '删除角色失败,请稍后重试');
+ }
+ };
+
+ const handleSetActiveCharacter = async (uuid: string) => {
+ try {
+ const response = await setActiveProfile(uuid);
+ if (response.code === 200) {
+ setProfiles(prev => prev.map(profile => ({
+ ...profile,
+ is_active: profile.uuid === uuid
+ })));
+ alert('角色切换成功!');
+ } else {
+ throw new Error(response.message || '设置活跃角色失败');
+ }
+ } catch (error) {
+ console.error('设置活跃角色失败:', error);
+ alert(error instanceof Error ? error.message : '设置活跃角色失败,请稍后重试');
+ }
+ };
+
+ const handleEditCharacter = async (uuid: string) => {
+ if (!editProfileName.trim()) {
+ alert('请输入角色名称');
+ return;
+ }
+
+ try {
+ const response = await updateProfile(uuid, { name: editProfileName.trim() });
+ if (response.code === 200) {
+ setProfiles(prev => prev.map(profile =>
+ profile.uuid === uuid ? response.data : profile
+ ));
+ setEditingProfile(null);
+ setEditProfileName('');
+ alert('角色编辑成功!');
+ } else {
+ throw new Error(response.message || '编辑角色失败');
+ }
+ } catch (error) {
+ console.error('编辑角色失败:', error);
+ alert(error instanceof Error ? error.message : '编辑角色失败,请稍后重试');
+ }
+ };
+
+ const handleDeleteSkin = async (skinId: number) => {
+ if (!confirm('确定要删除这个皮肤吗?')) return;
+
+ setMySkins(prev => prev.filter(skin => skin.id !== skinId));
+ };
+
+ const handleToggleSkinVisibility = async (skinId: number) => {
+ try {
+ const skin = mySkins.find(s => s.id === skinId);
+ if (!skin) return;
+
+ // TODO: 添加更新皮肤API调用
+ setMySkins(prev => prev.map(skin =>
+ skin.id === skinId ? { ...skin, is_public: !skin.is_public } : skin
+ ));
+ } catch (error) {
+ console.error('切换皮肤可见性失败:', error);
+ }
+ };
+
+ const handleToggleFavorite = async (skinId: number) => {
+ try {
+ const response = await toggleFavorite(skinId);
+ if (response.code === 200) {
+ // 重新加载收藏数据
+ const favoritesResponse = await getFavoriteTextures({ page: 1, page_size: 50 });
+ if (favoritesResponse.code === 200) {
+ setFavoriteSkins(favoritesResponse.data.list || []);
+ }
+ }
+ } catch (error) {
+ console.error('切换收藏状态失败:', error);
+ }
+ };
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setSelectedFile(file);
+ }
+ };
+
+ const handleUploadSkin = async () => {
+ if (!selectedFile || !newSkinData.name.trim()) {
+ alert('请选择皮肤文件并输入皮肤名称');
+ return;
+ }
+
+ setIsUploading(true);
+ setUploadProgress(0);
+
+ try {
+ // 使用直接上传接口
+ const progressInterval = setInterval(() => {
+ setUploadProgress(prev => Math.min(prev + 10, 80));
+ }, 200);
+
+ const response = await uploadTexture(selectedFile, {
+ name: newSkinData.name.trim(),
+ description: newSkinData.description.trim(),
+ type: newSkinData.type,
+ is_public: newSkinData.is_public,
+ is_slim: newSkinData.is_slim
+ });
+
+ clearInterval(progressInterval);
+ setUploadProgress(100);
+
+ if (response.code === 200) {
+ // 重新加载皮肤数据
+ const mySkinsResponse = await getMyTextures({ page: 1, page_size: 50 });
+ if (mySkinsResponse.code === 200) {
+ setMySkins(mySkinsResponse.data.list || []);
+ }
+
+ // 重置表单
+ setSelectedFile(null);
+ setNewSkinData({
+ name: '',
+ description: '',
+ type: 'SKIN',
+ is_public: false,
+ is_slim: false
+ });
+ setShowUploadSkin(false);
+ alert('皮肤上传成功!');
+ } else {
+ throw new Error(response.message || '上传皮肤失败');
+ }
+
+ } catch (error) {
+ console.error('上传皮肤失败:', error);
+ alert(error instanceof Error ? error.message : '上传皮肤失败,请稍后重试');
+ } finally {
+ setIsUploading(false);
+ setUploadProgress(0);
+ }
+ };
+
+ const handleAvatarFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // 验证文件类型和大小
+ if (!file.type.startsWith('image/')) {
+ alert('请选择图片文件');
+ return;
+ }
+ if (file.size > 2 * 1024 * 1024) {
+ alert('文件大小不能超过2MB');
+ return;
+ }
+ setAvatarFile(file);
+ }
+ };
+
+ const handleUploadAvatar = async () => {
+ if (!avatarFile) return;
+
+ setIsUploadingAvatar(true);
+ setAvatarUploadProgress(0);
+
+ try {
+ // 获取上传URL
+ const uploadUrlResponse = await generateAvatarUploadUrl(avatarFile.name);
+ if (uploadUrlResponse.code !== 200) {
+ throw new Error(uploadUrlResponse.message || '获取上传URL失败');
+ }
+
+ const { post_url, form_data, avatar_url } = uploadUrlResponse.data;
+
+ // 模拟上传进度
+ const progressInterval = setInterval(() => {
+ setAvatarUploadProgress(prev => Math.min(prev + 20, 80));
+ }, 200);
+
+ // 上传文件到预签名URL
+ const formData = new FormData();
+ Object.entries(form_data).forEach(([key, value]) => {
+ formData.append(key, value as string);
+ });
+ formData.append('file', avatarFile);
+
+ const uploadResponse = await fetch(post_url, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!uploadResponse.ok) {
+ throw new Error('文件上传失败');
+ }
+
+ clearInterval(progressInterval);
+ setAvatarUploadProgress(100);
+
+ // 更新用户头像URL
+ const updateResponse = await updateAvatarUrl(avatar_url);
+ if (updateResponse.code === 200) {
+ setUserProfile(prev => prev ? { ...prev, avatar: avatar_url } : null);
+ alert('头像上传成功!');
+ } else {
+ throw new Error(updateResponse.message || '更新头像URL失败');
+ }
+
+ } catch (error) {
+ console.error('头像上传失败:', error);
+ alert(error instanceof Error ? error.message : '头像上传失败,请稍后重试');
+ } finally {
+ setIsUploadingAvatar(false);
+ setAvatarUploadProgress(0);
+ setAvatarFile(null);
+ }
+ };
+
+ const handleDeleteAvatar = async () => {
+ if (!confirm('确定要删除头像吗?')) return;
+
+ try {
+ const response = await updateAvatarUrl('');
+ if (response.code === 200) {
+ setUserProfile(prev => prev ? { ...prev, avatar: undefined } : null);
+ alert('头像删除成功!');
+ } else {
+ throw new Error(response.message || '删除头像失败');
+ }
+ } catch (error) {
+ console.error('删除头像失败:', error);
+ alert(error instanceof Error ? error.message : '删除头像失败,请稍后重试');
+ }
+ };
+
+ const sidebarVariants = {
+ hidden: { x: -100, opacity: 0 },
+ visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" as const } }
+ };
+
+ const contentVariants = {
+ hidden: { x: 100, opacity: 0 },
+ visible: { x: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" as const, delay: 0.1 } }
+ };
+
+ if (!isAuthenticated) {
+ return (
+
+
+
+
+
+
+
+
请先登录
+
登录后即可查看个人资料
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+
加载中...
+
正在加载您的个人资料
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
+
+
+
加载失败
+
{error}
+
+
+ 重新加载
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Animated Background */}
+
+
+
+
+ {/* Left Sidebar - 完全固定的 */}
+
+ {/* User Profile Card */}
+
+
+ {userProfile?.avatar ? (
+

+ ) : (
+
+
+
+ )}
+
+
{userProfile?.username}
+
{userProfile?.email}
+
+
+
+
+
+
{mySkins.length}
+
皮肤
+
+
+
{favoriteSkins.length}
+
收藏
+
+
+
{userProfile?.points || 0}
+
积分
+
+
+
+
+ {/* Navigation Menu - 固定的 */}
+
+
+ {/* Logout Button - 始终在底部可见 */}
+
+
+ 退出登录
+
+
+
+ {/* Right Content Area - 考虑左侧fixed侧栏的空间 */}
+
+
+ {/* Characters Tab */}
+ {activeTab === 'characters' && (
+
+
+
角色管理
+
setShowCreateCharacter(true)}
+ className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+ 创建角色
+
+
+
+ {/* Create Character Modal */}
+
+ {showCreateCharacter && (
+
+
+
+
创建新角色
+
+
+
+
+
+ setNewCharacterName(e.target.value)}
+ className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
+ placeholder="请输入角色名称"
+ />
+
+
+
+
+
+
+
+
+ )}
+
+
+ {profiles.length === 0 ? (
+
+
+
暂无角色
+
创建你的第一个Minecraft角色吧!
+
+ ) : (
+
+ {profiles.map((profile) => (
+
+
+ {editingProfile === profile.uuid ? (
+ setEditProfileName(e.target.value)}
+ className="text-lg font-semibold bg-transparent border-b border-orange-500 focus:outline-none text-gray-900 dark:text-white"
+ onBlur={() => handleEditCharacter(profile.uuid)}
+ onKeyPress={(e) => e.key === 'Enter' && handleEditCharacter(profile.uuid)}
+ autoFocus
+ />
+ ) : (
+
{profile.name}
+ )}
+ {profile.is_active && (
+
+ 当前使用
+
+ )}
+
+
+
+
+
+ {!profile.is_active && (
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Skins Tab */}
+ {activeTab === 'skins' && (
+
+
+
我的皮肤
+ setShowUploadSkin(true)}
+ className="bg-gradient-to-r from-orange-500 to-amber-500 text-white px-4 py-2 rounded-xl flex items-center space-x-2 shadow-lg hover:shadow-xl transition-all duration-200"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+ 上传皮肤
+
+
+
+ {/* Upload Skin Modal */}
+
+ {showUploadSkin && (
+
+
+
+
上传皮肤
+
+
+
+
+
+
+
+
+ 点击选择文件或拖拽到此处
+
+
+
+ {selectedFile && (
+
+ 已选择: {selectedFile.name}
+
+ )}
+
+
+
+
+
+ setNewSkinData(prev => ({ ...prev, name: e.target.value }))}
+ className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent"
+ placeholder="请输入皮肤名称"
+ />
+
+
+
+
+
+
+
+
+ {isUploading && (
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+
+ {mySkins.length === 0 ? (
+
+
+
暂无皮肤
+
上传你的第一个Minecraft皮肤吧!
+
+ ) : (
+
+ {mySkins.map((skin) => (
+
+
+
+ {!skin.is_public && (
+
+ 私密
+
+ )}
+
+
+
+
{skin.name}
+
+ 上传于 {new Date(skin.created_at).toLocaleDateString()}
+
+
+
+
+
+ {skin.download_count}
+
+
+
+ {skin.favorite_count}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Favorites Tab */}
+ {activeTab === 'favorites' && (
+
+
+
收藏夹
+
+
+ {favoriteSkins.length === 0 ? (
+
+
+
暂无收藏
+
去发现一些喜欢的皮肤吧!
+
+ ) : (
+
+ {favoriteSkins.map((skin) => (
+
+
+
+
+
{skin.name}
+
+ 由 {skin.uploader_id} 上传
+
+
+
+
+
+ {skin.download_count}
+
+
+
+ {skin.favorite_count}
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Settings Tab */}
+ {activeTab === 'settings' && (
+
+ 账户设置
+
+
+ {/* Avatar Settings */}
+
+
+
+ 头像设置
+
+
+
+ {userProfile?.avatar ? (
+

+ ) : (
+
+
+
+ )}
+
document.getElementById('avatar-upload')?.click()}
+ >
+
+
+
+
+
+
+ 支持 JPG、PNG 格式,最大 2MB
+
+ {avatarFile && (
+
+
+ 已选择: {avatarFile.name}
+
+
+ )}
+ {isUploadingAvatar && (
+
+
+
+ 上传中... {avatarUploadProgress}%
+
+
+ )}
+
+
+
+ {isUploadingAvatar ? '上传中...' : '上传头像'}
+
+ {userProfile?.avatar && (
+
+ 删除头像
+
+ )}
+
+
+
+
+
+ {/* Basic Info */}
+
+
+
+ 基本信息
+
+
+
+
+
+ {/* Password Settings */}
+
+
+
+ 密码设置
+
+
+
+
+ {/* API Settings */}
+
+
+
+ API设置
+
+
+
+
+
+
+
+ 重新生成
+
+
+
+ 此密钥用于Minecraft客户端连接,请妥善保管
+
+
+
+
+
+ {/* Account Actions */}
+
+
+
+ 账户操作
+
+
+
+ 更换邮箱地址
+
+
+
+ 退出登录
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ );
+}
+
diff --git a/src/app/skins/page.tsx b/src/app/skins/page.tsx
new file mode 100644
index 0000000..218658e
--- /dev/null
+++ b/src/app/skins/page.tsx
@@ -0,0 +1,462 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { MagnifyingGlassIcon, EyeIcon, HeartIcon, ArrowDownTrayIcon, SparklesIcon, FunnelIcon, ArrowsUpDownIcon } from '@heroicons/react/24/outline';
+import { HeartIcon as HeartIconSolid } from '@heroicons/react/24/solid';
+import SkinViewer from '@/components/SkinViewer';
+import { searchTextures, toggleFavorite, type Texture } from '@/lib/api';
+import { useAuth } from '@/contexts/AuthContext';
+
+export default function SkinsPage() {
+ const [textures, setTextures] = useState([]);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [textureType, setTextureType] = useState<'SKIN' | 'CAPE' | 'ALL'>('ALL');
+ const [sortBy, setSortBy] = useState('最新');
+ const [isLoading, setIsLoading] = useState(true);
+ const [page, setPage] = useState(1);
+ const [total, setTotal] = useState(0);
+ const [totalPages, setTotalPages] = useState(1);
+ const [favoritedIds, setFavoritedIds] = useState>(new Set());
+ const { isAuthenticated } = useAuth();
+
+ const sortOptions = ['最新', '最热', '最多下载'];
+ const pageSize = 20;
+
+ // 加载材质数据
+ const loadTextures = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ console.log('开始加载材质数据,参数:', { searchTerm, textureType, sortBy, page, pageSize });
+
+ const response = await searchTextures({
+ keyword: searchTerm || undefined,
+ type: textureType !== 'ALL' ? textureType : undefined,
+ public_only: true,
+ page,
+ page_size: pageSize,
+ });
+
+ console.log('API响应数据:', response);
+ console.log('API响应code:', response.code);
+ console.log('API响应data:', response.data);
+
+ if (response.code === 200 && response.data) {
+ // 安全地处理数据,避免未定义错误
+ const textureList = response.data.list || [];
+ const totalCount = response.data.total || 0;
+ const totalPagesCount = response.data.total_pages || 1;
+
+ console.log('解析后的数据:', { textureList, totalCount, totalPagesCount });
+ console.log('材质列表长度:', textureList.length);
+
+ if (textureList.length > 0) {
+ console.log('第一个材质数据:', textureList[0]);
+ console.log('第一个材质URL:', textureList[0].url);
+ }
+
+ let sortedList = [...textureList];
+
+ // 客户端排序
+ if (sortedList.length > 0) {
+ switch (sortBy) {
+ case '最热':
+ sortedList = sortedList.sort((a, b) => (b.favorite_count || 0) - (a.favorite_count || 0));
+ break;
+ case '最多下载':
+ sortedList = sortedList.sort((a, b) => (b.download_count || 0) - (a.download_count || 0));
+ break;
+ default: // 最新
+ sortedList = sortedList.sort((a, b) => {
+ const dateA = new Date(a.created_at || 0).getTime();
+ const dateB = new Date(b.created_at || 0).getTime();
+ return dateB - dateA;
+ });
+ }
+ }
+
+ setTextures(sortedList);
+ setTotal(totalCount);
+ setTotalPages(totalPagesCount);
+
+ console.log('设置状态后的数据:', { sortedListLength: sortedList.length, totalCount, totalPagesCount });
+ } else {
+ // API返回错误状态
+ console.warn('API返回错误:', response.message);
+ console.warn('API完整响应:', response);
+ setTextures([]);
+ setTotal(0);
+ setTotalPages(1);
+ }
+ } catch (error) {
+ console.error('加载材质失败:', error);
+ // 发生网络或其他错误时,显示空状态
+ setTextures([]);
+ setTotal(0);
+ setTotalPages(1);
+ } finally {
+ setIsLoading(false);
+ console.log('加载完成,isLoading设置为false');
+ }
+ }, [searchTerm, textureType, sortBy, page]);
+
+ useEffect(() => {
+ loadTextures();
+ }, [loadTextures]);
+
+ // 处理收藏
+ const handleFavorite = async (id: number) => {
+ if (!isAuthenticated) {
+ alert('请先登录');
+ return;
+ }
+
+ try {
+ const response = await toggleFavorite(id);
+ if (response.code === 200) {
+ setFavoritedIds(prev => {
+ const newSet = new Set(prev);
+ if (response.data.is_favorited) {
+ newSet.add(id);
+ } else {
+ newSet.delete(id);
+ }
+ return newSet;
+ });
+ // 更新本地数据
+ setTextures(prev => prev.map(texture =>
+ texture.id === id
+ ? {
+ ...texture,
+ favorite_count: response.data.is_favorited
+ ? texture.favorite_count + 1
+ : Math.max(0, texture.favorite_count - 1)
+ }
+ : texture
+ ));
+ }
+ } catch (error) {
+ console.error('收藏操作失败:', error);
+ alert('操作失败,请稍后重试');
+ }
+ };
+
+ return (
+
+ {/* Animated Background - 保持背景但简化 */}
+
+
+
+ {/* 简化的头部区域 */}
+
+
+ 皮肤库
+
+
+ 发现和分享精彩的Minecraft皮肤与披风
+
+
+
+ {/* 重新设计的搜索区域 - 更紧凑专业 */}
+
+
+ {/* 搜索框 - 更紧凑 */}
+
+
+
+ {
+ setSearchTerm(e.target.value);
+ setPage(1);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ loadTextures();
+ }
+ }}
+ className="w-full pl-10 pr-4 py-2.5 border border-gray-200 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-orange-500 bg-white/80 dark:bg-gray-700/80 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 transition-all duration-200 hover:border-gray-300 dark:hover:border-gray-500"
+ />
+
+
+
+ {/* 类型筛选 - 更紧凑 */}
+
+
+
+
+
+
+
+ {/* 排序 - 更紧凑 */}
+
+
+
+
+
+
+
+ {/* 搜索按钮 - 更简洁 */}
+
+ 搜索
+
+
+
+
+ {/* 结果统计 - 更简洁 */}
+
+
+ 共找到 {total} 个材质
+
+ {totalPages > 1 && (
+
+ setPage(p => Math.max(1, p - 1))}
+ disabled={page === 1}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200"
+ whileHover={{ scale: page === 1 ? 1 : 1.05 }}
+ whileTap={{ scale: page === 1 ? 1 : 0.95 }}
+ >
+ 上一页
+
+
+ {page} / {totalPages}
+
+ setPage(p => Math.min(totalPages, p + 1))}
+ disabled={page === totalPages}
+ className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200"
+ whileHover={{ scale: page === totalPages ? 1 : 1.05 }}
+ whileTap={{ scale: page === totalPages ? 1 : 0.95 }}
+ >
+ 下一页
+
+
+ )}
+
+
+ {/* Loading State - 保持但简化 */}
+
+ {isLoading && (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Textures Grid - 保持卡片设计但简化 */}
+
+ {!isLoading && (
+
+ {textures.map((texture, index) => {
+ const isFavorited = favoritedIds.has(texture.id);
+ return (
+
+
+ {/* 3D Skin Preview */}
+
+ {texture.type === 'SKIN' ? (
+
+ ) : (
+
+ )}
+
+ {/* 标签 */}
+
+
+ {texture.type === 'SKIN' ? '皮肤' : '披风'}
+
+ {texture.is_slim && (
+
+ 细臂
+
+ )}
+
+
+
+ {/* Texture Info */}
+
+
{texture.name}
+ {texture.description && (
+
+ {texture.description}
+
+ )}
+
+ {/* Stats */}
+
+
+
+
+ {texture.favorite_count}
+
+
+
+ {texture.download_count}
+
+
+
+
+ {/* Action Buttons */}
+
+ window.open(texture.url, '_blank')}
+ className="flex-1 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200 font-medium shadow-md hover:shadow-lg"
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ 查看
+
+ handleFavorite(texture.id)}
+ className={`px-3 py-2 border rounded-lg transition-all duration-200 font-medium ${
+ isFavorited
+ ? 'bg-gradient-to-r from-red-500 to-pink-500 border-transparent text-white shadow-md'
+ : 'border-orange-500 text-orange-500 hover:bg-gradient-to-r hover:from-orange-500 hover:to-orange-600 hover:text-white hover:border-transparent hover:shadow-md'
+ }`}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ {isFavorited ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Empty State - 简化 */}
+
+ {!isLoading && textures.length === 0 && (
+
+
+
+
+ 没有找到相关材质
+ 尝试调整搜索条件
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/ErrorNotification.tsx b/src/components/ErrorNotification.tsx
new file mode 100644
index 0000000..221a626
--- /dev/null
+++ b/src/components/ErrorNotification.tsx
@@ -0,0 +1,194 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { XMarkIcon, ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from '@heroicons/react/24/outline';
+
+export type ErrorType = 'error' | 'warning' | 'success' | 'info';
+
+interface ErrorNotificationProps {
+ message: string;
+ type?: ErrorType;
+ duration?: number;
+ onClose?: () => void;
+}
+
+export function ErrorNotification({ message, type = 'error', duration = 5000, onClose }: ErrorNotificationProps) {
+ const [isVisible, setIsVisible] = useState(true);
+
+ useEffect(() => {
+ if (duration > 0) {
+ const timer = setTimeout(() => {
+ setIsVisible(false);
+ onClose?.();
+ }, duration);
+ return () => clearTimeout(timer);
+ }
+ }, [duration, onClose]);
+
+ const handleClose = () => {
+ setIsVisible(false);
+ onClose?.();
+ };
+
+ const getIcon = () => {
+ switch (type) {
+ case 'error':
+ return ;
+ case 'warning':
+ return ;
+ case 'success':
+ return ;
+ case 'info':
+ return ;
+ }
+ };
+
+ const getStyles = () => {
+ switch (type) {
+ case 'error':
+ return {
+ bg: 'bg-red-50 dark:bg-red-900/20',
+ border: 'border-red-200 dark:border-red-800',
+ text: 'text-red-800 dark:text-red-200',
+ icon: 'text-red-500',
+ close: 'text-red-400 hover:text-red-600 dark:text-red-300 dark:hover:text-red-100'
+ };
+ case 'warning':
+ return {
+ bg: 'bg-yellow-50 dark:bg-yellow-900/20',
+ border: 'border-yellow-200 dark:border-yellow-800',
+ text: 'text-yellow-800 dark:text-yellow-200',
+ icon: 'text-yellow-500',
+ close: 'text-yellow-400 hover:text-yellow-600 dark:text-yellow-300 dark:hover:text-yellow-100'
+ };
+ case 'success':
+ return {
+ bg: 'bg-green-50 dark:bg-green-900/20',
+ border: 'border-green-200 dark:border-green-800',
+ text: 'text-green-800 dark:text-green-200',
+ icon: 'text-green-500',
+ close: 'text-green-400 hover:text-green-600 dark:text-green-300 dark:hover:text-green-100'
+ };
+ case 'info':
+ return {
+ bg: 'bg-blue-50 dark:bg-blue-900/20',
+ border: 'border-blue-200 dark:border-blue-800',
+ text: 'text-blue-800 dark:text-blue-200',
+ icon: 'text-blue-500',
+ close: 'text-blue-400 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-100'
+ };
+ }
+ };
+
+ const styles = getStyles();
+
+ return (
+
+ {isVisible && (
+
+
+
+ {getIcon()}
+
+
+
+
+
+ )}
+
+ );
+}
+
+// 全局错误管理器
+class ErrorManager {
+ private static instance: ErrorManager;
+ private listeners: Array<(notification: ErrorNotificationProps & { id: string }) => void> = [];
+
+ static getInstance(): ErrorManager {
+ if (!ErrorManager.instance) {
+ ErrorManager.instance = new ErrorManager();
+ }
+ return ErrorManager.instance;
+ }
+
+ showError(message: string, duration?: number) {
+ this.showNotification(message, 'error', duration);
+ }
+
+ showWarning(message: string, duration?: number) {
+ this.showNotification(message, 'warning', duration);
+ }
+
+ showSuccess(message: string, duration?: number) {
+ this.showNotification(message, 'success', duration);
+ }
+
+ showInfo(message: string, duration?: number) {
+ this.showNotification(message, 'info', duration);
+ }
+
+ private showNotification(message: string, type: ErrorType, duration?: number) {
+ const notification = {
+ id: Math.random().toString(36).substr(2, 9),
+ message,
+ type,
+ duration: duration ?? (type === 'error' ? 5000 : 3000)
+ };
+
+ this.listeners.forEach(listener => listener(notification));
+ }
+
+ subscribe(listener: (notification: ErrorNotificationProps & { id: string }) => void) {
+ this.listeners.push(listener);
+ return () => {
+ this.listeners = this.listeners.filter(l => l !== listener);
+ };
+ }
+}
+
+export const errorManager = ErrorManager.getInstance();
+
+// 错误提示容器组件
+export function ErrorNotificationContainer() {
+ const [notifications, setNotifications] = useState>([]);
+
+ useEffect(() => {
+ const unsubscribe = errorManager.subscribe((notification) => {
+ setNotifications(prev => [...prev, notification]);
+ });
+
+ return unsubscribe;
+ }, []);
+
+ const removeNotification = (id: string) => {
+ setNotifications(prev => prev.filter(n => n.id !== id));
+ };
+
+ return (
+ <>
+ {notifications.map((notification) => (
+ removeNotification(notification.id)}
+ />
+ ))}
+ >
+ );
+}
diff --git a/src/components/ErrorPage.tsx b/src/components/ErrorPage.tsx
new file mode 100644
index 0000000..26ec3c6
--- /dev/null
+++ b/src/components/ErrorPage.tsx
@@ -0,0 +1,279 @@
+'use client';
+
+import Link from 'next/link';
+import { motion } from 'framer-motion';
+import {
+ HomeIcon,
+ ArrowLeftIcon,
+ ExclamationTriangleIcon,
+ XCircleIcon,
+ ClockIcon,
+ ServerIcon,
+ WifiIcon
+} from '@heroicons/react/24/outline';
+
+export interface ErrorPageProps {
+ code?: number;
+ title?: string;
+ message?: string;
+ description?: string;
+ type?: '404' | '500' | '403' | 'network' | 'timeout' | 'maintenance' | 'custom';
+ actions?: {
+ primary?: {
+ label: string;
+ href?: string;
+ onClick?: () => void;
+ };
+ secondary?: {
+ label: string;
+ href?: string;
+ onClick?: () => void;
+ };
+ };
+ showContact?: boolean;
+}
+
+const errorConfigs = {
+ '404': {
+ icon: ,
+ title: '页面不见了',
+ message: '抱歉,我们找不到您要访问的页面。',
+ description: '它可能已被移动、删除,或者您输入的链接不正确。'
+ },
+ '500': {
+ icon: ,
+ title: '服务器错误',
+ message: '抱歉,服务器遇到了一些问题。',
+ description: '我们的团队正在努力解决这个问题,请稍后再试。'
+ },
+ '403': {
+ icon: ,
+ title: '访问被拒绝',
+ message: '抱歉,您没有权限访问此页面。',
+ description: '请检查您的账户权限或联系管理员。'
+ },
+ 'network': {
+ icon: ,
+ title: '网络连接错误',
+ message: '无法连接到服务器。',
+ description: '请检查您的网络连接,然后重试。'
+ },
+ 'timeout': {
+ icon: ,
+ title: '请求超时',
+ message: '请求处理时间过长。',
+ description: '请刷新页面或稍后再试。'
+ },
+ 'maintenance': {
+ icon: ,
+ title: '系统维护中',
+ message: '我们正在进行系统维护。',
+ description: '请稍后再试,我们会尽快恢复服务。'
+ }
+};
+
+export function ErrorPage({
+ code,
+ title,
+ message,
+ description,
+ type = 'custom',
+ actions,
+ showContact = true
+}: ErrorPageProps) {
+ const config = errorConfigs[type] || {};
+ const displayTitle = title || config.title || '出错了';
+ const displayMessage = message || config.message || '发生了一些错误';
+ const displayDescription = description || config.description || '';
+
+ const defaultActions = {
+ primary: {
+ label: '返回主页',
+ href: '/'
+ },
+ secondary: {
+ label: '返回上页',
+ onClick: () => window.history.back()
+ }
+ };
+
+ const finalActions = { ...defaultActions, ...actions };
+
+ const getIconColor = () => {
+ switch (type) {
+ case '404': return 'text-orange-500';
+ case '500': return 'text-red-500';
+ case '403': return 'text-yellow-500';
+ case 'network': return 'text-blue-500';
+ case 'timeout': return 'text-purple-500';
+ case 'maintenance': return 'text-gray-500';
+ default: return 'text-orange-500';
+ }
+ };
+
+ const getCodeColor = () => {
+ switch (type) {
+ case '404': return 'from-orange-400 via-orange-500 to-amber-500';
+ case '500': return 'from-red-400 via-red-500 to-pink-500';
+ case '403': return 'from-yellow-400 via-yellow-500 to-orange-500';
+ case 'network': return 'from-blue-400 via-blue-500 to-cyan-500';
+ case 'timeout': return 'from-purple-400 via-purple-500 to-pink-500';
+ case 'maintenance': return 'from-gray-400 via-gray-500 to-slate-500';
+ default: return 'from-orange-400 via-orange-500 to-amber-500';
+ }
+ };
+
+ return (
+
+
+ {/* 错误代码 */}
+ {code && (
+
+
+ {code}
+
+
+
+ )}
+
+ {/* 图标 */}
+
+
+ {config.icon || }
+
+
+
+ {/* 错误信息 */}
+
+
+ {displayTitle}
+
+
+ {displayMessage}
+
+ {displayDescription && (
+
+ {displayDescription}
+
+ )}
+
+
+ {/* 操作按钮 */}
+
+ {finalActions.primary && (
+ finalActions.primary.href ? (
+
+
+ {finalActions.primary.label}
+
+ ) : (
+
+ )
+ )}
+
+ {finalActions.secondary && (
+ finalActions.secondary.href ? (
+
+
+ {finalActions.secondary.label}
+
+ ) : (
+
+ )
+ )}
+
+
+ {/* 联系信息 */}
+ {showContact && (
+
+ 如果问题持续存在,请
+
+ 联系我们
+
+ 的支持团队
+
+
+ )}
+
+
+ {/* 背景装饰 */}
+
+
+ );
+}
+
+// 预设的错误页面组件
+export function NotFoundPage() {
+ return ;
+}
+
+export function ServerErrorPage() {
+ return ;
+}
+
+export function ForbiddenPage() {
+ return ;
+}
+
+export function NetworkErrorPage() {
+ return ;
+}
+
+export function TimeoutErrorPage() {
+ return ;
+}
+
+export function MaintenancePage() {
+ return ;
+}
diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx
new file mode 100644
index 0000000..e4c0a39
--- /dev/null
+++ b/src/components/MainContent.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+
+export function MainContent({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+ const isAuthPage = pathname === '/auth';
+ const isHomePage = pathname === '/';
+
+ return (
+
+ {children}
+
+ );
+}
+
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
new file mode 100644
index 0000000..2601bec
--- /dev/null
+++ b/src/components/Navbar.tsx
@@ -0,0 +1,387 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import Link from 'next/link';
+import { useRouter, usePathname } from 'next/navigation';
+import { Bars3Icon, XMarkIcon, UserCircleIcon } from '@heroicons/react/24/outline';
+import { motion, AnimatePresence } from 'framer-motion';
+import { useAuth } from '@/contexts/AuthContext';
+
+export default function Navbar() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isHidden, setIsHidden] = useState(false);
+ const [isScrolled, setIsScrolled] = useState(false);
+ const [showScrollTop, setShowScrollTop] = useState(false);
+ const [navbarHeight, setNavbarHeight] = useState(0);
+ const navbarRef = useRef(null);
+ const { user, isAuthenticated, logout } = useAuth();
+ const router = useRouter();
+ const pathname = usePathname();
+
+ // 在auth页面隐藏navbar
+ const isAuthPage = pathname === '/auth';
+
+ // 检测navbar高度并设置CSS自定义属性
+ useEffect(() => {
+ const updateHeight = () => {
+ if (navbarRef.current) {
+ const height = navbarRef.current.offsetHeight;
+ setNavbarHeight(height);
+ document.documentElement.style.setProperty('--navbar-height', `${height}px`);
+ }
+ };
+
+ updateHeight();
+ window.addEventListener('resize', updateHeight);
+ return () => window.removeEventListener('resize', updateHeight);
+ }, []);
+
+ useEffect(() => {
+ let lastScrollY = 0;
+ let ticking = false;
+
+ const handleScroll = () => {
+ if (!ticking) {
+ window.requestAnimationFrame(() => {
+ const currentScrollY = window.scrollY;
+
+ // 检测是否滚动到顶部
+ setIsScrolled(currentScrollY > 20);
+
+ // 显示返回顶部按钮(滚动超过300px)
+ setShowScrollTop(currentScrollY > 300);
+
+ // 更敏感的隐藏逻辑:只要往下滚动就隐藏,不管滚动多少
+ if (!isAuthPage && currentScrollY > lastScrollY && currentScrollY > 10) {
+ setIsHidden(true);
+ } else if (currentScrollY < lastScrollY) { // 往上滚动就显示
+ setIsHidden(false);
+ }
+
+ lastScrollY = currentScrollY;
+ ticking = false;
+ });
+ ticking = true;
+ }
+ };
+
+ if (!isAuthPage) {
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ }
+
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, [isAuthPage]);
+
+ const handleLogout = () => {
+ logout();
+ router.push('/');
+ setIsOpen(false);
+ };
+
+ const handleLinkClick = () => {
+ setIsOpen(false);
+ };
+
+ // 在auth页面不渲染navbar
+ if (isAuthPage) {
+ return null;
+ }
+
+ const scrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ };
+
+ return (
+
+
+
+ {/* Logo */}
+
+
+
+ C
+
+
+ CarrotSkin
+
+
+
+
+ {/* Desktop Navigation */}
+
+
+
+ 首页
+
+
+
+
+
+
+ 皮肤库
+
+
+
+
+
+ {/* 用户头像框 - 类似知乎和哔哩哔哩的设计 */}
+ {isAuthenticated ? (
+
+
+
+ {user?.avatar ? (
+
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ {user?.username}
+
+
+
+
+
+
+ 退出登录
+
+
+ ) : (
+
+
+
+
+
+
+ 登录
+
+
+ )}
+
+
+ {/* Mobile menu button */}
+
+ setIsOpen(!isOpen)}
+ className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-colors duration-200 p-2"
+ whileHover={{ scale: 1.1 }}
+ whileTap={{ scale: 0.9 }}
+ >
+ {isOpen ? : }
+
+
+
+
+
+ {/* Mobile Navigation */}
+
+ {isOpen && (
+
+
+
+
+ 首页
+
+
+
+
+
+ 皮肤库
+
+
+
+
+
+ {isAuthenticated ? (
+ <>
+
+
+
+ {user?.avatar ? (
+

+ ) : (
+
+ )}
+
{user?.username}
+
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+ 登录
+
+
+
+
+
+ 注册
+
+
+ >
+ )}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx
new file mode 100644
index 0000000..65e6d5a
--- /dev/null
+++ b/src/components/ScrollToTop.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+
+export default function ScrollToTop() {
+ const [showScrollTop, setShowScrollTop] = useState(false);
+
+ useEffect(() => {
+ let ticking = false;
+
+ const handleScroll = () => {
+ if (!ticking) {
+ window.requestAnimationFrame(() => {
+ const currentScrollY = window.scrollY;
+ // 显示返回顶部按钮(滚动超过300px)
+ setShowScrollTop(currentScrollY > 300);
+ ticking = false;
+ });
+ ticking = true;
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
+ const scrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ };
+
+ return (
+
+ {showScrollTop && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/SkinViewer.tsx b/src/components/SkinViewer.tsx
new file mode 100644
index 0000000..254667d
--- /dev/null
+++ b/src/components/SkinViewer.tsx
@@ -0,0 +1,218 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { SkinViewer as SkinViewer3D, WalkingAnimation, FunctionAnimation } from 'skinview3d';
+
+interface SkinViewerProps {
+ skinUrl: string;
+ capeUrl?: string;
+ isSlim?: boolean;
+ width?: number;
+ height?: number;
+ className?: string;
+ autoRotate?: boolean;
+ walking?: boolean;
+}
+
+export default function SkinViewer({
+ skinUrl,
+ capeUrl,
+ isSlim = false,
+ width = 300,
+ height = 300,
+ className = '',
+ autoRotate = true,
+ walking = false,
+}: SkinViewerProps) {
+ const canvasRef = useRef(null);
+ const viewerRef = useRef(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [hasError, setHasError] = useState(false);
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ // 预加载皮肤图片以检查是否可访问
+ useEffect(() => {
+ if (!skinUrl) return;
+
+ setIsLoading(true);
+ setHasError(false);
+ setImageLoaded(false);
+
+ const img = new Image();
+ img.crossOrigin = 'anonymous'; // 尝试跨域访问
+
+ img.onload = () => {
+ console.log('皮肤图片加载成功:', skinUrl);
+ setImageLoaded(true);
+ setIsLoading(false);
+ };
+
+ img.onerror = (error) => {
+ console.error('皮肤图片加载失败:', skinUrl, error);
+ setHasError(true);
+ setIsLoading(false);
+ };
+
+ // 开始加载图片
+ img.src = skinUrl;
+
+ return () => {
+ // 清理
+ img.onload = null;
+ img.onerror = null;
+ };
+ }, [skinUrl]);
+
+ // 初始化3D查看器
+ useEffect(() => {
+ if (!canvasRef.current || !imageLoaded || hasError) return;
+
+ try {
+ console.log('初始化3D皮肤查看器:', { skinUrl, isSlim, width, height });
+
+ // 使用canvas的实际尺寸,参考blessingskin
+ const canvas = canvasRef.current;
+ const viewer = new SkinViewer3D({
+ canvas: canvas,
+ width: canvas.clientWidth || width,
+ height: canvas.clientHeight || height,
+ skin: skinUrl,
+ cape: capeUrl,
+ model: isSlim ? 'slim' : 'default',
+ zoom: 1.0, // 使用blessingskin的zoom方式
+ });
+
+ viewerRef.current = viewer;
+
+ // 设置背景和控制选项 - 参考blessingskin
+ viewer.background = null; // 透明背景
+ viewer.autoRotate = false; // 禁用自动旋转
+
+ // 禁用所有交互控制
+ viewer.controls.enableRotate = false; // 禁用旋转控制
+ viewer.controls.enableZoom = false; // 禁用缩放
+ viewer.controls.enablePan = false; // 禁用平移
+
+ console.log('3D皮肤查看器初始化成功');
+
+ } catch (error) {
+ console.error('3D皮肤查看器初始化失败:', error);
+ setHasError(true);
+ }
+
+ // 清理函数
+ return () => {
+ if (viewerRef.current) {
+ try {
+ viewerRef.current.dispose();
+ viewerRef.current = null;
+ console.log('3D皮肤查看器已清理');
+ } catch (error) {
+ console.error('清理3D皮肤查看器失败:', error);
+ }
+ }
+ };
+ }, [skinUrl, capeUrl, isSlim, width, height, autoRotate, walking, imageLoaded, hasError]);
+
+ // 当皮肤URL改变时更新
+ useEffect(() => {
+ if (viewerRef.current && skinUrl && imageLoaded) {
+ try {
+ console.log('更新皮肤URL:', skinUrl);
+ viewerRef.current.loadSkin(skinUrl);
+ } catch (error) {
+ console.error('更新皮肤失败:', error);
+ }
+ }
+ }, [skinUrl, imageLoaded]);
+
+ // 监听容器尺寸变化并调整viewer大小 - 参考blessingskin
+ useEffect(() => {
+ if (viewerRef.current && canvasRef.current) {
+ const rect = canvasRef.current.getBoundingClientRect();
+ viewerRef.current.setSize(rect.width, rect.height);
+ }
+ }, [imageLoaded]); // 当图片加载完成后调整尺寸
+
+ // 当披风URL改变时更新
+ useEffect(() => {
+ if (viewerRef.current && capeUrl && imageLoaded) {
+ try {
+ console.log('更新披风URL:', capeUrl);
+ viewerRef.current.loadCape(capeUrl);
+ } catch (error) {
+ console.error('更新披风失败:', error);
+ }
+ } else if (viewerRef.current && !capeUrl && imageLoaded) {
+ try {
+ viewerRef.current.loadCape(null);
+ } catch (error) {
+ console.error('移除披风失败:', error);
+ }
+ }
+ }, [capeUrl, imageLoaded]);
+
+ // 当模型类型改变时更新
+ useEffect(() => {
+ if (viewerRef.current && skinUrl && imageLoaded) {
+ try {
+ console.log('更新模型类型:', isSlim ? 'slim' : 'default');
+ viewerRef.current.loadSkin(skinUrl, { model: isSlim ? 'slim' : 'default' });
+ } catch (error) {
+ console.error('更新模型失败:', error);
+ }
+ }
+ }, [isSlim, skinUrl, imageLoaded]);
+
+ // 错误状态显示
+ if (hasError) {
+ return (
+
+
+
⚠️
+
+ 皮肤加载失败
+
+
+ 图片链接可能无效或存在访问限制
+
+
+
+ );
+ }
+
+ // 加载状态显示
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..0e6f21f
--- /dev/null
+++ b/src/contexts/AuthContext.tsx
@@ -0,0 +1,223 @@
+'use client';
+
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+
+interface User {
+ id: number;
+ username: string;
+ email: string;
+ avatar?: string;
+ points: number;
+ role: string;
+ status: number;
+ last_login_at?: string;
+ created_at: string;
+ updated_at: string;
+ totalSkins?: number;
+ totalDownloads?: number;
+}
+
+interface AuthContextType {
+ user: User | null;
+ isLoading: boolean;
+ isAuthenticated: boolean;
+ login: (username: string, password: string) => Promise;
+ register: (username: string, email: string, password: string, verificationCode: string) => Promise;
+ logout: () => void;
+ updateUser: (userData: Partial) => void;
+ refreshUser: () => Promise;
+}
+
+const API_BASE_URL = 'http://localhost:8080/api/v1';
+
+const AuthContext = createContext(undefined);
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Check if user is logged in on mount
+ useEffect(() => {
+ const checkAuthStatus = async () => {
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ await refreshUser();
+ } else {
+ setIsLoading(false);
+ }
+ };
+
+ checkAuthStatus();
+ }, []);
+
+ const setAuthToken = (token: string) => {
+ localStorage.setItem('authToken', token);
+ };
+
+ const getAuthHeaders = () => {
+ const token = localStorage.getItem('authToken');
+ return {
+ 'Content-Type': 'application/json',
+ 'Authorization': token ? `Bearer ${token}` : '',
+ };
+ };
+
+ const login = async (username: string, password: string) => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`${API_BASE_URL}/auth/login`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ username,
+ password,
+ }),
+ });
+
+ const result = await response.json();
+
+ if (result.code === 200) {
+ const { token, user_info } = result.data;
+ setAuthToken(token);
+
+ const userData: User = {
+ id: user_info.id,
+ username: user_info.username,
+ email: user_info.email,
+ avatar: user_info.avatar,
+ points: user_info.points,
+ role: user_info.role,
+ status: user_info.status,
+ last_login_at: user_info.last_login_at,
+ created_at: user_info.created_at,
+ updated_at: user_info.updated_at,
+ };
+
+ setUser(userData);
+ } else {
+ throw new Error(result.message || '登录失败,请检查用户名和密码');
+ }
+ } catch (error) {
+ throw error instanceof Error ? error : new Error('登录失败,请检查用户名和密码');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const register = async (username: string, email: string, password: string, verificationCode: string) => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`${API_BASE_URL}/auth/register`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ username,
+ email,
+ password,
+ verification_code: verificationCode,
+ }),
+ });
+
+ const result = await response.json();
+
+ if (result.code === 200) {
+ const { token, user_info } = result.data;
+ setAuthToken(token);
+
+ const userData: User = {
+ id: user_info.id,
+ username: user_info.username,
+ email: user_info.email,
+ avatar: user_info.avatar,
+ points: user_info.points,
+ role: user_info.role,
+ status: user_info.status,
+ created_at: user_info.created_at,
+ updated_at: user_info.updated_at,
+ };
+
+ setUser(userData);
+ } else {
+ throw new Error(result.message || '注册失败,请稍后重试');
+ }
+ } catch (error) {
+ throw error instanceof Error ? error : new Error('注册失败,请稍后重试');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const refreshUser = async () => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/user/profile`, {
+ method: 'GET',
+ headers: getAuthHeaders(),
+ });
+
+ const result = await response.json();
+
+ if (result.code === 200) {
+ const user_info = result.data;
+ const userData: User = {
+ id: user_info.id,
+ username: user_info.username,
+ email: user_info.email,
+ avatar: user_info.avatar,
+ points: user_info.points,
+ role: user_info.role,
+ status: user_info.status,
+ last_login_at: user_info.last_login_at,
+ created_at: user_info.created_at,
+ updated_at: user_info.updated_at,
+ };
+
+ setUser(userData);
+ } else {
+ // Token invalid or expired
+ logout();
+ }
+ } catch (error) {
+ console.error('Failed to refresh user:', error);
+ logout();
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const logout = () => {
+ setUser(null);
+ localStorage.removeItem('authToken');
+ };
+
+ const updateUser = (userData: Partial) => {
+ if (user) {
+ setUser({ ...user, ...userData });
+ }
+ };
+
+ const value: AuthContextType = {
+ user,
+ isLoading,
+ isAuthenticated: !!user,
+ login,
+ register,
+ logout,
+ updateUser,
+ refreshUser,
+ };
+
+ return {children};
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
+
diff --git a/src/lib/api.ts b/src/lib/api.ts
new file mode 100644
index 0000000..092b6ec
--- /dev/null
+++ b/src/lib/api.ts
@@ -0,0 +1,303 @@
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080/api/v1';
+
+export interface Texture {
+ id: number;
+ uploader_id: number;
+ name: string;
+ description?: string;
+ type: 'SKIN' | 'CAPE';
+ url: string;
+ hash: string;
+ size: number;
+ is_public: boolean;
+ download_count: number;
+ favorite_count: number;
+ is_slim: boolean;
+ status: number;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface Profile {
+ uuid: string;
+ user_id: number;
+ name: string;
+ skin_id?: number;
+ cape_id?: number;
+ is_active: boolean;
+ last_used_at?: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface PaginatedResponse {
+ list: T[];
+ total: number;
+ page: number;
+ page_size: number;
+ total_pages: number;
+}
+
+export interface ApiResponse {
+ code: number;
+ message: string;
+ data: T;
+}
+
+// 获取认证头
+function getAuthHeaders(): HeadersInit {
+ const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
+ return {
+ 'Content-Type': 'application/json',
+ ...(token && { Authorization: `Bearer ${token}` }),
+ };
+}
+
+// 搜索材质
+export async function searchTextures(params: {
+ keyword?: string;
+ type?: 'SKIN' | 'CAPE';
+ public_only?: boolean;
+ page?: number;
+ page_size?: number;
+}): Promise>> {
+ const queryParams = new URLSearchParams();
+ if (params.keyword) queryParams.append('keyword', params.keyword);
+ if (params.type) queryParams.append('type', params.type);
+ if (params.public_only !== undefined) queryParams.append('public_only', String(params.public_only));
+ if (params.page) queryParams.append('page', String(params.page));
+ if (params.page_size) queryParams.append('page_size', String(params.page_size));
+
+ const response = await fetch(`${API_BASE_URL}/texture?${queryParams.toString()}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ return response.json();
+}
+
+// 获取材质详情
+export async function getTexture(id: number): Promise> {
+ const response = await fetch(`${API_BASE_URL}/texture/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ return response.json();
+}
+
+// 切换收藏状态
+export async function toggleFavorite(id: number): Promise> {
+ const response = await fetch(`${API_BASE_URL}/texture/${id}/favorite`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
+// 获取用户上传的材质列表
+export async function getMyTextures(params: {
+ page?: number;
+ page_size?: number;
+}): Promise>> {
+ const queryParams = new URLSearchParams();
+ if (params.page) queryParams.append('page', String(params.page));
+ if (params.page_size) queryParams.append('page_size', String(params.page_size));
+
+ const response = await fetch(`${API_BASE_URL}/texture/my?${queryParams.toString()}`, {
+ method: 'GET',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
+// 获取用户收藏的材质列表
+export async function getFavoriteTextures(params: {
+ page?: number;
+ page_size?: number;
+}): Promise>> {
+ const queryParams = new URLSearchParams();
+ if (params.page) queryParams.append('page', String(params.page));
+ if (params.page_size) queryParams.append('page_size', String(params.page_size));
+
+ const response = await fetch(`${API_BASE_URL}/texture/favorites?${queryParams.toString()}`, {
+ method: 'GET',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
+// 获取用户档案列表
+export async function getProfiles(): Promise> {
+ const response = await fetch(`${API_BASE_URL}/profile`, {
+ method: 'GET',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
+// 创建档案
+export async function createProfile(name: string): Promise> {
+ const response = await fetch(`${API_BASE_URL}/profile`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ name }),
+ });
+
+ return response.json();
+}
+
+// 更新档案
+export async function updateProfile(uuid: string, data: {
+ name?: string;
+ skin_id?: number;
+ cape_id?: number;
+}): Promise> {
+ const response = await fetch(`${API_BASE_URL}/profile/${uuid}`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify(data),
+ });
+
+ return response.json();
+}
+
+// 删除档案
+export async function deleteProfile(uuid: string): Promise> {
+ const response = await fetch(`${API_BASE_URL}/profile/${uuid}`, {
+ method: 'DELETE',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
+// 设置活跃档案
+export async function setActiveProfile(uuid: string): Promise> {
+ const response = await fetch(`${API_BASE_URL}/profile/${uuid}/activate`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
+// 获取用户信息
+export async function getUserProfile(): Promise> {
+ const response = await fetch(`${API_BASE_URL}/user/profile`, {
+ method: 'GET',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
+// 更新用户信息
+export async function updateUserProfile(data: {
+ avatar?: string;
+ old_password?: string;
+ new_password?: string;
+}): Promise> {
+ const response = await fetch(`${API_BASE_URL}/user/profile`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify(data),
+ });
+
+ return response.json();
+}
+
+// 直接上传皮肤文件
+export async function uploadTexture(file: File, data: {
+ name: string;
+ description?: string;
+ type?: 'SKIN' | 'CAPE';
+ is_public?: boolean;
+ is_slim?: boolean;
+}): Promise> {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('name', data.name);
+ if (data.description) formData.append('description', data.description);
+ if (data.type) formData.append('type', data.type);
+ if (data.is_public !== undefined) formData.append('is_public', String(data.is_public));
+ if (data.is_slim !== undefined) formData.append('is_slim', String(data.is_slim));
+
+ const response = await fetch(`${API_BASE_URL}/texture/upload`, {
+ method: 'POST',
+ headers: {
+ ...(typeof window !== 'undefined' ? { Authorization: `Bearer ${localStorage.getItem('authToken')}` } : {}),
+ },
+ body: formData,
+ });
+
+ return response.json();
+}
+
+// 生成头像上传URL
+export async function generateAvatarUploadUrl(fileName: string): Promise;
+ avatar_url: string;
+ expires_in: number;
+}>> {
+ const response = await fetch(`${API_BASE_URL}/user/avatar/upload-url`, {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ file_name: fileName }),
+ });
+
+ return response.json();
+}
+
+// 更新头像URL
+export async function updateAvatarUrl(avatarUrl: string): Promise> {
+ const response = await fetch(`${API_BASE_URL}/user/avatar?avatar_url=${encodeURIComponent(avatarUrl)}`, {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ });
+
+ return response.json();
+}
+
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..eae9d14
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,51 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: [
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ orange: {
+ 50: '#fff7ed',
+ 100: '#ffedd5',
+ 200: '#fed7aa',
+ 300: '#fdba74',
+ 400: '#fb923c',
+ 500: '#f97316',
+ 600: '#ea580c',
+ 700: '#c2410c',
+ 800: '#9a3412',
+ 900: '#7c2d12',
+ 950: '#431407',
+ },
+ minecraft: {
+ grass: '#7cbd6a',
+ dirt: '#8b4513',
+ stone: '#808080',
+ wood: '#8b6914',
+ diamond: '#4fc3f7',
+ }
+ },
+ fontFamily: {
+ 'minecraft': ['Minecraft', 'monospace'],
+ 'sans': ['Inter', 'system-ui', 'sans-serif'],
+ },
+ animation: {
+ 'float': 'float 3s ease-in-out infinite',
+ 'pulse-slow': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+ },
+ keyframes: {
+ float: {
+ '0%, 100%': { transform: 'translateY(0px)' },
+ '50%': { transform: 'translateY(-10px)' },
+ }
+ }
+ },
+ },
+ plugins: [],
+};
+export default config;