commit 75900a7dcf63d7a510e1b2a2b04c7b51ea822c2d Author: Your Name Date: Mon Sep 22 15:40:57 2025 +0800 Initial commit from Create Next App diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +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). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +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 + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +: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); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..21b686d --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,103 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.tsx + + . +
  2. +
  3. + Save and see your changes instantly. +
  4. +
+ +
+ + Vercel logomark + Deploy now + + + Read our docs + +
+
+ +
+ ); +} diff --git a/docs/API文档.md b/docs/API文档.md new file mode 100644 index 0000000..d6eebb7 --- /dev/null +++ b/docs/API文档.md @@ -0,0 +1,867 @@ +# 账户管理系统 API 文档 + +## 概述 + +本文档描述了账户管理系统的API接口,用于Web前端对接。系统提供了账户的获取、上传、更新、删除和统计等功能。 + +### 基础信息 + +- **基础URL**: `{API_BASE_URL}` +- **数据格式**: JSON +- **字符编码**: UTF-8 +- **认证方式**: 需要在请求头中包含认证信息 + +### 通用响应格式 + +所有API接口都使用统一的响应格式: + +```typescript +interface ApiResponse { + code: BusinessCode; // 业务状态码 + message: string; // 响应消息 + data: T | null; // 响应数据 +} +``` + +#### 业务状态码 (BusinessCode) + +| 状态码 | 名称 | 说明 | +|--------|------|------| +| 0 | Success | 请求成功 | +| 1001 | NoResource | 资源不存在 | +| 2001 | InvalidParams | 参数无效 | +| 3001 | ResourceConflict | 资源冲突 | +| 4001 | PermissionDenied | 权限不足 | +| 5001 | BusinessError | 业务错误 | + +## 数据模型 + +### 账户 (Account) + +账户是系统的核心数据模型,包含以下字段: + +```typescript +interface Account { + id: number; // 账户ID,自增主键 + ownerId: string; // 所有者ID + platform: string; // 平台名称 + customId: string; // 自定义ID + data: string; // 账户数据,JSON格式字符串 + status: string; // 账户状态 + notes?: string; // 备注,可选 + lockedAt?: Date; // 锁定时间,可选 + createdAt: Date; // 创建时间 + updatedAt: Date; // 更新时间 +} +``` + +#### 账户状态 + +- `available`: 可用 +- `locked`: 已锁定 +- 其他自定义状态 + +### 分页结果 (PaginationResult) + +```typescript +interface PaginationResult { + page: number; // 当前页码 + pageSize: number; // 每页条数 + total: number; // 总记录数 + totalPages: number; // 总页数 +} +``` + +### 统计概览 (StatsOverview) + +```typescript +interface StatsOverview { + totalAccounts: number; // 总账户数 + platformSummary: Record; // 平台统计 + ownerSummary: Record; // 所有者统计 + statusSummary: Record; // 状态统计 + detailedBreakdown: { // 详细统计 + platform: string; + ownerId: string; + status: string; + count: number; + }[]; +} +``` + +## API 接口 + +### 1. 账户获取接口 + +用于脚本端获取可用账户。 + +#### 请求 + +```http +GET /s/v1/{ownerId}/acquire?platform={platform}&count={count} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ownerId | 路径 | string | 是 | 所有者ID | +| platform | 查询 | string | 是 | 平台名称 | +| count | 查询 | number | 否 | 获取数量,默认1,最大100 | + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully acquired 1 account.", + "data": [ + { + "id": 1, + "customId": "custom123", + "data": "{\"username\":\"test\",\"password\":\"123456\"}" + } + ] +} +``` + +无可用账户响应: + +```json +{ + "code": 1001, + "message": "No available accounts found for platform 'example'.", + "data": null +} +``` + +#### 示例 + +```javascript +// 使用fetch获取账户 +async function acquireAccounts(ownerId, platform, count = 1) { + const response = await fetch(`/s/v1/${ownerId}/acquire?platform=${platform}&count=${count}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + } + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await acquireAccounts('owner123', 'example', 2); +if (result.code === 0) { + console.log('获取成功:', result.data); +} else { + console.error('获取失败:', result.message); +} +``` + +### 2. 账户状态更新接口 + +用于脚本端更新账户状态。 + +#### 请求 + +```http +GET /s/v1/{ownerId}/update/{accountId}/{newStatus}?notes={notes} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ownerId | 路径 | string | 是 | 所有者ID | +| accountId | 路径 | number | 是 | 账户ID | +| newStatus | 路径 | string | 是 | 新状态 | +| notes | 查询 | string | 否 | 备注 | + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Account status updated to 'used'.", + "data": { + "updatedId": 1 + } +} +``` + +权限不足响应: + +```json +{ + "code": 4001, + "message": "Account not found or permission denied.", + "data": null +} +``` + +#### 示例 + +```javascript +// 使用fetch更新账户状态 +async function updateAccountStatus(ownerId, accountId, newStatus, notes) { + const url = `/s/v1/${ownerId}/update/${accountId}/${newStatus}`; + if (notes) { + url += `?notes=${encodeURIComponent(notes)}`; + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + } + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await updateAccountStatus('owner123', 1, 'used', '测试使用'); +if (result.code === 0) { + console.log('更新成功:', result.data); +} else { + console.error('更新失败:', result.message); +} +``` + +### 3. 账户上传接口 + +用于脚本端批量上传账户。 + +#### 请求 + +```http +POST /s/v1/{ownerId}/upload +``` + +#### 请求体 + +```json +[ + { + "platform": "string", + "customId": "string", + "data": "string", + "status": "string" + } +] +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ownerId | 路径 | string | 是 | 所有者ID | +| body | 请求体 | ScriptUploadItem[] | 是 | 账户数据数组 | + +#### ScriptUploadItem 类型 + +```typescript +interface ScriptUploadItem { + platform: string; // 平台名称 + customId: string; // 自定义ID + data: string; // 账户数据,JSON格式字符串 + status?: string; // 账户状态,可选,默认为"available" +} +``` + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully processed 2 accounts (1 created, 1 updated).", + "data": { + "processedCount": 2, + "createdCount": 1, + "updatedCount": 1 + } +} +``` + +#### 示例 + +```javascript +// 使用fetch上传账户 +async function uploadAccounts(ownerId, accounts) { + const response = await fetch(`/s/v1/${ownerId}/upload`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify(accounts) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const accounts = [ + { + platform: 'example', + customId: 'user123', + data: JSON.stringify({ username: 'user123', password: 'pass123' }), + status: 'available' + }, + { + platform: 'example', + customId: 'user456', + data: JSON.stringify({ username: 'user456', password: 'pass456' }) + } +]; + +const result = await uploadAccounts('owner123', accounts); +if (result.code === 0) { + console.log('上传成功:', result.data); +} else { + console.error('上传失败:', result.message); +} +``` + +### 4. 账户列表接口 + +用于Web端获取账户列表,支持筛选、分页和排序。 + +#### 请求 + +```http +POST /web/v1/accounts/list +``` + +#### 请求体 + +```json +{ + "filters": { + "platform": "string", + "status": ["string"], + "ownerId": "string", + "search": "string" + }, + "pagination": { + "page": 1, + "pageSize": 10 + }, + "sort": { + "field": "id", + "order": "desc" + } +} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| filters | 请求体 | ListAccountsFilters | 否 | 筛选条件 | +| pagination | 请求体 | object | 是 | 分页参数 | +| sort | 请求体 | object | 是 | 排序参数 | + +#### ListAccountsFilters 类型 + +```typescript +interface ListAccountsFilters { + platform?: string; // 平台名称筛选 + status?: string[]; // 状态筛选,多选 + ownerId?: string; // 所有者ID筛选 + search?: string; // 搜索关键词,搜索customId和notes +} +``` + +#### 排序字段 + +支持以下字段排序: +- `id`: 账户ID +- `ownerId`: 所有者ID +- `platform`: 平台名称 +- `customId`: 自定义ID +- `data`: 账户数据 +- `status`: 账户状态 +- `notes`: 备注 +- `lockedAt`: 锁定时间 +- `createdAt`: 创建时间 +- `updatedAt`: 更新时间 + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Success", + "data": { + "list": [ + { + "id": 1, + "ownerId": "owner123", + "platform": "example", + "customId": "user123", + "data": "{\"username\":\"user123\",\"password\":\"pass123\"}", + "status": "available", + "notes": "测试账户", + "lockedAt": null, + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } + } +} +``` + +#### 示例 + +```javascript +// 使用fetch获取账户列表 +async function getAccountsList(filters, pagination, sort) { + const response = await fetch('/web/v1/accounts/list', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify({ + filters, + pagination, + sort + }) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const filters = { + platform: 'example', + status: ['available'], + search: 'test' +}; + +const pagination = { + page: 1, + pageSize: 10 +}; + +const sort = { + field: 'createdAt', + order: 'desc' +}; + +const result = await getAccountsList(filters, pagination, sort); +if (result.code === 0) { + console.log('账户列表:', result.data.list); + console.log('分页信息:', result.data.pagination); +} else { + console.error('获取失败:', result.message); +} +``` + +### 5. 批量删除账户接口 + +用于Web端批量删除账户。 + +#### 请求 + +```http +POST /web/v1/accounts/delete-batch +``` + +#### 请求体 + +```json +{ + "ids": [1, 2, 3] +} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ids | 请求体 | number[] | 是 | 要删除的账户ID数组 | + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully deleted 3 accounts.", + "data": { + "deletedCount": 3 + } +} +``` + +#### 示例 + +```javascript +// 使用fetch批量删除账户 +async function batchDeleteAccounts(ids) { + const response = await fetch('/web/v1/accounts/delete-batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify({ ids }) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await batchDeleteAccounts([1, 2, 3]); +if (result.code === 0) { + console.log('删除成功:', result.data); +} else { + console.error('删除失败:', result.message); +} +``` + +### 6. 批量更新账户接口 + +用于Web端批量更新账户。 + +#### 请求 + +```http +POST /web/v1/accounts/update-batch +``` + +#### 请求体 + +```json +{ + "ids": [1, 2, 3], + "payload": { + "status": "available", + "ownerId": "newOwner", + "notes": "批量更新备注" + } +} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ids | 请求体 | number[] | 是 | 要更新的账户ID数组 | +| payload | 请求体 | object | 是 | 更新数据 | + +#### payload 类型 + +```typescript +interface UpdatePayload { + status?: string; // 新状态 + ownerId?: string; // 新所有者ID + notes?: string; // 新备注 +} +``` + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully updated 3 accounts.", + "data": { + "updatedCount": 3 + } +} +``` + +#### 示例 + +```javascript +// 使用fetch批量更新账户 +async function batchUpdateAccounts(ids, payload) { + const response = await fetch('/web/v1/accounts/update-batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify({ ids, payload }) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const ids = [1, 2, 3]; +const payload = { + status: 'available', + notes: '批量更新为可用状态' +}; + +const result = await batchUpdateAccounts(ids, payload); +if (result.code === 0) { + console.log('更新成功:', result.data); +} else { + console.error('更新失败:', result.message); +} +``` + +### 7. 统计概览接口 + +用于Web端获取账户统计信息。 + +#### 请求 + +```http +GET /web/v1/stats/overview +``` + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Success", + "data": { + "totalAccounts": 100, + "platformSummary": { + "example": 50, + "test": 30, + "demo": 20 + }, + "ownerSummary": { + "owner1": 40, + "owner2": 35, + "owner3": 25 + }, + "statusSummary": { + "available": 70, + "locked": 20, + "used": 10 + }, + "detailedBreakdown": [ + { + "platform": "example", + "ownerId": "owner1", + "status": "available", + "count": 20 + }, + { + "platform": "example", + "ownerId": "owner1", + "status": "locked", + "count": 10 + } + ] + } +} +``` + +#### 示例 + +```javascript +// 使用fetch获取统计概览 +async function getStatsOverview() { + const response = await fetch('/web/v1/stats/overview', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + } + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await getStatsOverview(); +if (result.code === 0) { + console.log('总账户数:', result.data.totalAccounts); + console.log('平台统计:', result.data.platformSummary); + console.log('所有者统计:', result.data.ownerSummary); + console.log('状态统计:', result.data.statusSummary); + console.log('详细统计:', result.data.detailedBreakdown); +} else { + console.error('获取失败:', result.message); +} +``` + +## 错误处理 + +所有API接口在发生错误时都会返回统一的错误响应格式: + +```json +{ + "code": "错误码", + "message": "错误信息", + "data": null +} +``` + +### 常见错误码 + +| 错误码 | 说明 | 处理建议 | +|--------|------|----------| +| 1001 | 资源不存在 | 检查请求的资源是否存在 | +| 2001 | 参数无效 | 检查请求参数是否符合要求 | +| 3001 | 资源冲突 | 检查是否有重复数据 | +| 4001 | 权限不足 | 检查认证信息是否正确 | +| 5001 | 业务错误 | 检查业务逻辑是否正确 | + +## 最佳实践 + +1. **认证**:所有API请求都需要在请求头中包含认证信息,例如: + ```http + Authorization: Bearer YOUR_TOKEN + ``` + +2. **错误处理**:前端应该根据返回的`code`值进行相应的错误处理,而不是依赖`message`字段。 + +3. **分页**:在获取列表数据时,合理设置分页大小,建议不超过100条。 + +4. **批量操作**:批量操作时,建议限制单次操作的数量,避免服务器压力过大。 + +5. **数据格式**:账户数据字段`data`存储的是JSON格式字符串,前端需要进行相应的序列化和反序列化操作。 + +## 类型定义 + +以下是完整的TypeScript类型定义,可以直接在前端项目中使用: + +```typescript +// 业务状态码 +enum BusinessCode { + Success = 0, + NoResource = 1001, + InvalidParams = 2001, + ResourceConflict = 3001, + PermissionDenied = 4001, + BusinessError = 5001, +} + +// 通用响应格式 +interface ApiResponse { + code: BusinessCode; + message: string; + data: T | null; +} + +// 账户类型 +interface Account { + id: number; + ownerId: string; + platform: string; + customId: string; + data: string; + status: string; + notes?: string; + lockedAt?: Date | null; + createdAt: Date; + updatedAt: Date; +} + +// 分页结果 +interface PaginationResult { + page: number; + pageSize: number; + total: number; + totalPages: number; +} + +// 脚本获取响应 +type ScriptAcquireResponse = Pick[]; + +// 脚本上传项 +interface ScriptUploadItem { + platform: string; + customId: string; + data: string; + status?: string; +} + +// 账户列表筛选条件 +interface ListAccountsFilters { + platform?: string; + status?: string[]; + ownerId?: string; + search?: string; +} + +// 账户列表请求体 +interface ListAccountsBody { + filters: ListAccountsFilters; + pagination: { + page: number; + pageSize: number; + }; + sort: { + field: keyof Account; + order: 'asc' | 'desc'; + }; +} + +// 账户列表响应 +interface ListAccountsResponse { + list: Account[]; + pagination: PaginationResult; +} + +// 批量删除请求体 +interface BatchDeleteBody { + ids: number[]; +} + +// 批量更新请求体 +interface BatchUpdateBody { + ids: number[]; + payload: Partial>; +} + +// 统计概览 +interface StatsOverview { + totalAccounts: number; + platformSummary: Record; + ownerSummary: Record; + statusSummary: Record; + detailedBreakdown: { + platform: string; + ownerId: string; + status: string; + count: number; + }[]; +} +``` + +## 版本历史 + +| 版本 | 日期 | 说明 | +|------|------|------| +| 1.0.0 | 2023-01-01 | 初始版本 | \ No newline at end of file diff --git a/docs/后端设计.md b/docs/后端设计.md new file mode 100644 index 0000000..35b8e11 --- /dev/null +++ b/docs/后端设计.md @@ -0,0 +1,328 @@ +### **轻量化账号管理平台 - 工程设计蓝图** + +#### 1. 核心设计哲学 + +* **单表驱动**: 所有核心数据聚合在 `accounts` 表,简化数据库模型。 +* **OwnerID 即身份**: `ownerId` 同时作为拥有者标识和API认证凭证。 +* **配置硬编码**: 关键状态(如 `available`, `locked`)硬编码在业务逻辑中,降低系统复杂性。 +* **职责分离的API**: 为自动化脚本和管理后台提供两套独立的、高度优化的API。 +* **规范化通信**: 所有API遵循统一的响应结构,业务结果通过 `code` 字段传递。 +* **高性能与健壮性**: 依赖PostgreSQL原生特性(事务、行锁、索引)和专用的后台任务确保系统稳定。 + +#### 2. 技术栈 + +* **运行时**: Node.js (v18+) +* **语言**: TypeScript +* **Web 框架**: **Fastify** +* **数据库**: PostgreSQL (v14+) +* **ORM/查询构建器**: **Drizzle ORM** +* **数据校验**: **Zod** + +#### 3. 项目结构 + +采用模块化、可扩展的目录结构,清晰分离各项职责。 + +``` +. +├── drizzle/ # Drizzle ORM 迁移文件 +├── src/ +│ ├── api/ # API 路由定义 +│ │ └── v1/ +│ │ ├── web/ # 前端管理后台 API +│ │ │ ├── accounts.ts # 账号增删改查 +│ │ │ └── stats.ts # 统计数据 +│ │ └── script/ # 自动化脚本 API +│ │ └── actions.ts # 获取、更新、上传 +│ ├── core/ # 核心业务逻辑 (Services) +│ │ └── AccountService.ts # 封装所有与账号相关的业务操作 +│ ├── db/ # 数据库配置与 Schema +│ │ ├── index.ts # Drizzle 客户端实例 +│ │ └── schema.ts # 数据库表定义 +│ ├── jobs/ # 后台定时任务 +│ │ └── staleLockCleanup.ts # 清理超时锁定的任务 +│ ├── lib/ # 通用库、工具函数 +│ │ └── apiResponse.ts # 统一响应格式的辅助函数 +│ ├── types/ # 全局类型定义 +│ │ ├── api.ts # API 请求/响应体类型 +│ │ └── index.ts # 核心业务模型类型 +│ ├── index.ts # 应用入口,启动 Fastify 服务器 +│ └── config.ts # 应用配置 (数据库连接、端口等) +├── .env # 环境变量 +├── .env.example +├── package.json +└── tsconfig.json +``` + +#### 4. 数据库设计 (单表模型) + +```typescript +// file: src/db/schema.ts +import { pgTable, serial, varchar, timestamp, uniqueIndex, text, index } from 'drizzle-orm/pg-core'; + +export const accounts = pgTable('accounts', { + id: serial('id').primaryKey(), + ownerId: varchar('owner_id', { length: 128 }).notNull(), + platform: varchar('platform', { length: 100 }).notNull(), + customId: varchar('custom_id', { length: 255 }).notNull(), + data: text('data').notNull(), + status: varchar('status', { length: 50 }).notNull(), + notes: text('notes'), + lockedAt: timestamp('locked_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}, (table) => ({ + platformCustomIdIdx: uniqueIndex('platform_custom_id_idx').on(table.platform, table.customId), + ownerIdStatusIdx: index('owner_id_status_idx').on(table.ownerId, table.status), + platformOwnerIdx: index('platform_owner_idx').on(table.platform, table.ownerId), +})); +``` + +#### 5. 核心类型与接口定义 + +##### 5.1. 业务模型 + +```typescript +// file: src/types/index.ts +import { accounts } from '../db/schema'; +import { InferSelectModel } from 'drizzle-orm'; + +// 从数据库 Schema 自动推断出的 Account 类型,确保与数据库一致 +export type Account = InferSelectModel; +``` + +##### 5.2. API 通信规范 + +```typescript +// file: src/types/api.ts + +/** + * 业务状态码枚举,避免使用魔法数字 + */ +export enum BusinessCode { + Success = 0, + NoResource = 1001, + InvalidParams = 2001, + ResourceConflict = 3001, + PermissionDenied = 4001, // 业务层权限拒绝 + BusinessError = 5001, +} + +/** + * 统一的 API 响应体结构 + */ +export interface ApiResponse { + code: BusinessCode; + message: string; + data: T | null; +} + +/** + * 分页查询结果 + */ +export interface PaginationResult { + page: number; + pageSize: number; + total: number; + totalPages: number; +} +``` + +##### 5.3. API 请求/响应体类型 + +```typescript +// file: src/types/api.ts + +// --- 脚本 API --- + +export type ScriptAcquireResponse = Pick[]; + +export interface ScriptUploadItem { + platform: string; + customId: string; + data: string; + status?: string; +} + +// --- 前端管理 API --- + +export interface ListAccountsFilters { + platform?: string; + status?: string[]; + ownerId?: string; + search?: string; // 模糊搜索 customId 或 notes +} + +export interface ListAccountsBody { + filters: ListAccountsFilters; + pagination: { + page: number; + pageSize: number; + }; + sort: { + field: keyof Account; + order: 'asc' | 'desc'; + }; +} + +export interface ListAccountsResponse { + list: Account[]; + pagination: PaginationResult; +} + +export interface BatchDeleteBody { + ids: number[]; +} + +export interface BatchUpdateBody { + ids: number[]; + payload: Partial>; +} + +export interface StatsOverview { + totalAccounts: number; + platformSummary: Record; + ownerSummary: Record; + statusSummary: Record; + detailedBreakdown: { + platform: string; + ownerId: string; + status: string; + count: number; + }[]; +} +``` + +--- + +### **6. API 设计** + +#### A. 脚本专用 API (Base URL: `/s/v1/{ownerId}`) + +**认证**: 所有请求通过 URL 中的 `{ownerId}` 进行身份验证和授权。 + +1. **获取账号** + * **Endpoint**: `GET /acquire` + * **描述**: 获取一个或多个可用的账号,并将其状态锁定。 + * **Query Params**: + * `platform` (string, **必须**): 平台标识。 + * `count` (number, 可选, 默认 1): 获取数量。 + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 0, // BusinessCode.Success + "message": "Successfully acquired 2 accounts.", + "data": [ + { "id": 101, "customId": "user1@gmail.com", "data": "cookie_string_1" }, + { "id": 102, "customId": "user2@gmail.com", "data": "cookie_string_2" } + ] + } + ``` + * **业务失败响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 1001, // BusinessCode.NoResource + "message": "No available accounts found for platform 'google'.", + "data": null + } + ``` + +2. **更新账号状态** + * **Endpoint**: `GET /update/{accountId}/{newStatus}` + * **描述**: 快速更新指定账号的状态。 + * **Query Params**: + * `notes` (string, 可选): 添加备注信息。 + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ updatedId: number }> + { + "code": 0, + "message": "Account status updated to 'banned'.", + "data": { "updatedId": 12345 } + } + ``` + * **传输错误响应 (403 Forbidden)**: 当 URL 中的 `ownerId` 与 `accountId` 对应的账号所有者不匹配时返回。 + +3. **上传/更新账号** + * **Endpoint**: `POST /upload` + * **描述**: 批量创建或更新账号。 + * **Request Body**: `ScriptUploadItem[]` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ processedCount: number, createdCount: number, updatedCount: number }> + { + "code": 0, + "message": "Successfully processed 10 accounts (5 created, 5 updated).", + "data": { "processedCount": 10, "createdCount": 5, "updatedCount": 5 } + } + ``` + +#### B. 前端管理 API (Base URL: `/web/v1`) + +1. **获取账号列表 (复杂查询)** + * **Endpoint**: `POST /accounts/list` + * **描述**: 支持多维度筛选、分页和排序的账号查询。 + * **Request Body**: `ListAccountsBody` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 0, + "message": "Success", + "data": { + "list": [ /* ... Account 对象数组 ... */ ], + "pagination": { "page": 1, "pageSize": 50, "total": 123, "totalPages": 3 } + } + } + ``` + +2. **批量删除账号** + * **Endpoint**: `POST /accounts/delete-batch` + * **描述**: 根据 ID 列表批量删除账号。 + * **Request Body**: `BatchDeleteBody` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ deletedCount: number }> + { + "code": 0, + "message": "Successfully deleted 5 accounts.", + "data": { "deletedCount": 5 } + } + ``` + +3. **批量更新账号** + * **Endpoint**: `POST /accounts/update-batch` + * **描述**: 批量修改账号的状态、所有者或备注。 + * **Request Body**: `BatchUpdateBody` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ updatedCount: number }> + { + "code": 0, + "message": "Successfully updated 3 accounts.", + "data": { "updatedCount": 3 } + } + ``` + +4. **核心统计接口** + * **Endpoint**: `GET /stats/overview` + * **描述**: 一次性获取仪表盘所需的全部聚合统计数据。 + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 0, + "message": "Success", + "data": { /* ... StatsOverview 对象 ... */ } + } + ``` + +### **7. 关键后台任务** + +* **任务名称**: Stale Lock Cleanup (超时锁定清理) +* **执行频率**: 建议每 1 分钟执行一次。 +* **核心逻辑**: + * 查找所有 `status` 为 `locked` 且 `lockedAt` 时间早于预设阈值(例如 5 分钟前)的账号。 + * 执行 SQL: `UPDATE accounts SET status = 'available', lockedAt = NULL WHERE status = 'locked' AND lockedAt < NOW() - INTERVAL '5 minutes';` +* **目的**: 自动释放因脚本异常中断而未能解锁的账号,保证账号资源的流转性。 \ No newline at end of file diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b705a85 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "accounts-manager-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build --turbopack", + "start": "next start" + }, + "dependencies": { + "react": "19.1.0", + "react-dom": "19.1.0", + "next": "15.5.3" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@tailwindcss/postcss": "^4", + "tailwindcss": "^4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3d28a5c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,993 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + next: + specifier: 15.5.3 + version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.13 + '@types/node': + specifier: ^20 + version: 20.19.17 + '@types/react': + specifier: ^19 + version: 19.1.13 + '@types/react-dom': + specifier: ^19 + version: 19.1.9(@types/react@19.1.13) + tailwindcss: + specifier: ^4 + version: 4.1.13 + typescript: + specifier: ^5 + version: 5.9.2 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@next/env@15.5.3': + resolution: {integrity: sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==} + + '@next/swc-darwin-arm64@15.5.3': + resolution: {integrity: sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.3': + resolution: {integrity: sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.3': + resolution: {integrity: sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@15.5.3': + resolution: {integrity: sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@15.5.3': + resolution: {integrity: sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@15.5.3': + resolution: {integrity: sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@15.5.3': + resolution: {integrity: sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.3': + resolution: {integrity: sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.13': + resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} + + '@tailwindcss/oxide-android-arm64@4.1.13': + resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.13': + resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.13': + resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.13': + resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.13': + resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.13': + resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.13': + resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.13': + resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==} + + '@types/node@20.19.17': + resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==} + + '@types/react-dom@19.1.9': + resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.13': + resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} + + caniuse-lite@1.0.30001743: + resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + detect-libc@2.1.0: + resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@15.5.3: + resolution: {integrity: sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + tailwindcss@4.1.13: + resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} + + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + engines: {node: '>=6'} + + tar@7.4.4: + resolution: {integrity: sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.3 + optional: true + + '@img/sharp-darwin-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + optional: true + + '@img/sharp-linux-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.3 + optional: true + + '@img/sharp-linux-arm@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.3 + optional: true + + '@img/sharp-linux-ppc64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.3 + optional: true + + '@img/sharp-linux-s390x@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.3 + optional: true + + '@img/sharp-linux-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + optional: true + + '@img/sharp-wasm32@0.34.4': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.4': + optional: true + + '@img/sharp-win32-ia32@0.34.4': + optional: true + + '@img/sharp-win32-x64@0.34.4': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@next/env@15.5.3': {} + + '@next/swc-darwin-arm64@15.5.3': + optional: true + + '@next/swc-darwin-x64@15.5.3': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.3': + optional: true + + '@next/swc-linux-arm64-musl@15.5.3': + optional: true + + '@next/swc-linux-x64-gnu@15.5.3': + optional: true + + '@next/swc-linux-x64-musl@15.5.3': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.3': + optional: true + + '@next/swc-win32-x64-msvc@15.5.3': + optional: true + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.13': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.13 + + '@tailwindcss/oxide-android-arm64@4.1.13': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.13': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.13': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.13': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.13': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.13': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.13': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.13': + optional: true + + '@tailwindcss/oxide@4.1.13': + dependencies: + detect-libc: 2.1.0 + tar: 7.4.4 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-arm64': 4.1.13 + '@tailwindcss/oxide-darwin-x64': 4.1.13 + '@tailwindcss/oxide-freebsd-x64': 4.1.13 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.13 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.13 + '@tailwindcss/oxide-linux-x64-musl': 4.1.13 + '@tailwindcss/oxide-wasm32-wasi': 4.1.13 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 + + '@tailwindcss/postcss@4.1.13': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.13 + '@tailwindcss/oxide': 4.1.13 + postcss: 8.5.6 + tailwindcss: 4.1.13 + + '@types/node@20.19.17': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.1.9(@types/react@19.1.13)': + dependencies: + '@types/react': 19.1.13 + + '@types/react@19.1.13': + dependencies: + csstype: 3.1.3 + + caniuse-lite@1.0.30001743: {} + + chownr@3.0.0: {} + + client-only@0.0.1: {} + + csstype@3.1.3: {} + + detect-libc@2.1.0: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.3 + + graceful-fs@4.2.11: {} + + jiti@2.5.1: {} + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.0 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + nanoid@3.3.11: {} + + next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@next/env': 15.5.3 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001743 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.3 + '@next/swc-darwin-x64': 15.5.3 + '@next/swc-linux-arm64-gnu': 15.5.3 + '@next/swc-linux-arm64-musl': 15.5.3 + '@next/swc-linux-x64-gnu': 15.5.3 + '@next/swc-linux-x64-musl': 15.5.3 + '@next/swc-win32-arm64-msvc': 15.5.3 + '@next/swc-win32-x64-msvc': 15.5.3 + sharp: 0.34.4 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + picocolors@1.1.1: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react@19.1.0: {} + + scheduler@0.26.0: {} + + semver@7.7.2: + optional: true + + sharp@0.34.4: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.0 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 + optional: true + + source-map-js@1.2.1: {} + + styled-jsx@5.1.6(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + + tailwindcss@4.1.13: {} + + tapable@2.2.3: {} + + tar@7.4.4: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + tslib@2.8.1: {} + + typescript@5.9.2: {} + + undici-types@6.21.0: {} + + yallist@5.0.0: {} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}