perf: 优化批量上传
This commit is contained in:
110
AGENTS.md
Normal file
110
AGENTS.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Meme Studio is a homophone pun game operation platform built with Next.js 14 (App Router). It provides level configuration management for a wordplay game.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev # Start development server (port 3001)
|
||||||
|
pnpm run build # Production build
|
||||||
|
pnpm run deploy # Build and deploy to production server (./deploy.sh)
|
||||||
|
pnpm run lint # Run ESLint
|
||||||
|
|
||||||
|
# Database (Prisma + MySQL)
|
||||||
|
pnpm run db:generate # Generate Prisma client
|
||||||
|
pnpm run db:push # Push schema changes (dev)
|
||||||
|
pnpm run db:migrate # Create migration
|
||||||
|
pnpm run db:studio # Open Prisma Studio
|
||||||
|
pnpm run db:seed # Create/update admin user
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **Framework**: Next.js 14 App Router
|
||||||
|
- **Auth**: Better Auth with Prisma adapter (email/password)
|
||||||
|
- **Database**: MySQL via Prisma ORM
|
||||||
|
- **UI**: shadcn/ui + Tailwind CSS
|
||||||
|
- **State**: TanStack Query for server state
|
||||||
|
- **Drag & Drop**: @dnd-kit/sortable
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
**Route Groups**:
|
||||||
|
- `app/(auth)/` - Login page (no sidebar)
|
||||||
|
- `app/(dashboard)/` - Protected pages with sidebar layout
|
||||||
|
|
||||||
|
**Auth Flow**:
|
||||||
|
- `lib/auth.ts` - Server-side Better Auth config
|
||||||
|
- `lib/auth-client.ts` - Client-side auth hooks (`useSession`, `signIn`, `signOut`)
|
||||||
|
- `middleware.ts` - Cookie-based session check (cannot use Prisma in Edge Runtime)
|
||||||
|
|
||||||
|
**API Routes**:
|
||||||
|
- `/api/auth/[...all]` - Better Auth endpoints
|
||||||
|
- `/api/levels` - CRUD for game levels
|
||||||
|
- `/api/levels/reorder` - Batch update sort order
|
||||||
|
- `/api/cos/temp-key` - Tencent COS temporary credentials
|
||||||
|
|
||||||
|
**Database Models**:
|
||||||
|
- `Level` - Game levels with image, answer, hints, sortOrder
|
||||||
|
- `User`, `Session`, `Account`, `Verification` - Better Auth models
|
||||||
|
|
||||||
|
### basePath & Reverse Proxy
|
||||||
|
|
||||||
|
The app is deployed behind a reverse proxy at `/studio` path:
|
||||||
|
- `next.config.js` sets `basePath: '/studio'`
|
||||||
|
- All pages and API routes are served under `/studio/...`
|
||||||
|
|
||||||
|
**Critical gotchas**:
|
||||||
|
- **Next.js strips basePath from `request.url` in route handlers** — Better Auth's `basePath` must be `/api/auth` (without `/studio`), not `/studio/api/auth`
|
||||||
|
- **`BETTER_AUTH_URL` must NOT contain a path** (just the origin like `http://localhost:3001`) — Better Auth's `withPath()` silently ignores the `basePath` config if the URL already has a path component
|
||||||
|
- **HTTPS production adds `__Secure-` cookie prefix** — middleware must check both `better-auth.session_token` and `__Secure-better-auth.session_token`
|
||||||
|
- **`router.push()` auto-prepends basePath** — never manually add `/studio` to paths used in client-side navigation or callbackUrl params
|
||||||
|
- **`request.nextUrl.pathname` in middleware excludes basePath** — e.g., `/levels` not `/studio/levels`
|
||||||
|
- Prisma requires `binaryTargets = ["native", "rhel-openssl-3.0.x"]` for cross-platform deployment (macOS dev → Linux server)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
- **Server**: `root@119.91.211.52` at `/root/apps/meme-studio`
|
||||||
|
- **Process Manager**: PM2 (`ecosystem.config.js`)
|
||||||
|
- **Script**: `./deploy.sh` — builds locally, rsyncs files (excluding node_modules), installs deps on server, restarts PM2
|
||||||
|
- **Standalone output**: `next.config.js` sets `output: 'standalone'`
|
||||||
|
- Server uses `npm install --production` + `npx prisma generate` (prisma is a devDependency)
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Required in `.env`:
|
||||||
|
```
|
||||||
|
DATABASE_URL=mysql://...
|
||||||
|
BETTER_AUTH_SECRET= # 32+ chars, generate with: openssl rand -base64 32
|
||||||
|
BETTER_AUTH_URL= # Origin only, NO path (e.g., http://localhost:3001)
|
||||||
|
NEXT_PUBLIC_APP_URL= # Same as BETTER_AUTH_URL
|
||||||
|
NEXT_PUBLIC_BASE_PATH= # /studio
|
||||||
|
ADMIN_EMAIL=
|
||||||
|
ADMIN_PASSWORD=
|
||||||
|
COS_SECRET_ID= # Tencent Cloud COS
|
||||||
|
COS_SECRET_KEY=
|
||||||
|
COS_BUCKET=
|
||||||
|
COS_REGION=
|
||||||
|
COS_APPID=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Middleware uses cookie check only (Prisma doesn't work in Edge Runtime)
|
||||||
|
- Password hashing must use `hashPassword` from `better-auth/crypto` for compatibility
|
||||||
|
- Session model requires `token` field with unique constraint
|
||||||
|
- **Git commit messages must be written in Chinese**
|
||||||
|
|
||||||
|
|
||||||
|
<claude-mem-context>
|
||||||
|
# Memory Context
|
||||||
|
|
||||||
|
# $CMEM MemeStudio 2026-05-01 9:31am GMT+8
|
||||||
|
|
||||||
|
No previous sessions found.
|
||||||
|
</claude-mem-context>
|
||||||
@@ -60,18 +60,133 @@ interface RiddleMetadata {
|
|||||||
anchor_text?: string
|
anchor_text?: string
|
||||||
answer?: string
|
answer?: string
|
||||||
hints?: string[]
|
hints?: string[]
|
||||||
|
homophone_explanation?: string
|
||||||
riddle?: {
|
riddle?: {
|
||||||
homophone_explanation?: string
|
homophone_explanation?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RiddleConfigItem {
|
||||||
|
riddle_id?: string
|
||||||
|
answer?: string
|
||||||
|
hints?: string[]
|
||||||
|
homophone_explanation?: string
|
||||||
|
anchor_text?: string
|
||||||
|
reference_image?: string
|
||||||
|
riddle_image?: string
|
||||||
|
}
|
||||||
|
|
||||||
function truncate(s: string | undefined, max: number): string {
|
function truncate(s: string | undefined, max: number): string {
|
||||||
if (!s) return ''
|
if (!s) return ''
|
||||||
return s.length > max ? s.slice(0, max) : s
|
return s.length > max ? s.slice(0, max) : s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ANSWER_MAX_LENGTH = 8
|
||||||
|
|
||||||
|
function normalizePath(path: string): string {
|
||||||
|
return path.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinPath(base: string, path: string): string {
|
||||||
|
const cleanPath = normalizePath(path)
|
||||||
|
if (!base) return cleanPath
|
||||||
|
return normalizePath(`${base}/${cleanPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRelativePathWithoutRoot(file: FileWithPath): string {
|
||||||
|
const rel = normalizePath(file.webkitRelativePath)
|
||||||
|
const [, ...rest] = rel.split('/')
|
||||||
|
return rest.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilePathMap(files: FileList): Map<string, FileWithPath> {
|
||||||
|
const fileByPath = new Map<string, FileWithPath>()
|
||||||
|
for (let i = 0; i < files.length; i += 1) {
|
||||||
|
const file = files[i] as FileWithPath
|
||||||
|
const rel = normalizePath(file.webkitRelativePath)
|
||||||
|
if (!rel) continue
|
||||||
|
fileByPath.set(rel, file)
|
||||||
|
const withoutRoot = getRelativePathWithoutRoot(file)
|
||||||
|
if (withoutRoot) fileByPath.set(withoutRoot, file)
|
||||||
|
}
|
||||||
|
return fileByPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateItem(item: ParsedItem) {
|
||||||
|
if (!item.answer) {
|
||||||
|
item.parseError = '未解析到 answer'
|
||||||
|
} else if (item.answer.length > ANSWER_MAX_LENGTH) {
|
||||||
|
item.parseError = `答案超过 ${ANSWER_MAX_LENGTH} 字(${item.answer.length})`
|
||||||
|
} else if (!item.referenceFile || !item.riddleFile) {
|
||||||
|
item.parseError = '未匹配到 reference_image 或 riddle_image 对应图片'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseConfigJson(
|
||||||
|
jsonFile: FileWithPath,
|
||||||
|
fileByPath: Map<string, FileWithPath>
|
||||||
|
): Promise<ParsedItem[]> {
|
||||||
|
const text = await jsonFile.text()
|
||||||
|
const json = JSON.parse(text) as unknown
|
||||||
|
if (!Array.isArray(json)) return []
|
||||||
|
|
||||||
|
const jsonPath = normalizePath(jsonFile.webkitRelativePath)
|
||||||
|
const jsonDir = jsonPath.split('/').slice(0, -1).join('/')
|
||||||
|
const items: ParsedItem[] = []
|
||||||
|
|
||||||
|
json.forEach((raw, index) => {
|
||||||
|
const config = raw as RiddleConfigItem
|
||||||
|
if (!config || typeof config !== 'object') return
|
||||||
|
|
||||||
|
const referencePath = config.reference_image || ''
|
||||||
|
const riddlePath = config.riddle_image || ''
|
||||||
|
const reference = fileByPath.get(joinPath(jsonDir, referencePath)) || fileByPath.get(normalizePath(referencePath))
|
||||||
|
const riddle = fileByPath.get(joinPath(jsonDir, riddlePath)) || fileByPath.get(normalizePath(riddlePath))
|
||||||
|
const id = config.riddle_id || `${jsonFile.name}-${index + 1}`
|
||||||
|
const folderName = config.riddle_id || referencePath.split('/').slice(-2, -1)[0] || `第 ${index + 1} 关`
|
||||||
|
const hints = Array.isArray(config.hints) ? config.hints : []
|
||||||
|
|
||||||
|
const item: ParsedItem = {
|
||||||
|
id,
|
||||||
|
folderName,
|
||||||
|
referenceFile: reference,
|
||||||
|
riddleFile: riddle,
|
||||||
|
referencePreview: reference ? URL.createObjectURL(reference) : undefined,
|
||||||
|
riddlePreview: riddle ? URL.createObjectURL(riddle) : undefined,
|
||||||
|
anchorText: truncate(config.anchor_text, 500),
|
||||||
|
answer: (config.answer || '').trim(),
|
||||||
|
punchline: (config.homophone_explanation || '').trim(),
|
||||||
|
hint1: (hints[0] || '').trim(),
|
||||||
|
hint2: (hints[1] || '').trim(),
|
||||||
|
hint3: (hints[2] || '').trim(),
|
||||||
|
status: 'pending',
|
||||||
|
}
|
||||||
|
|
||||||
|
validateItem(item)
|
||||||
|
items.push(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
async function parseFolders(files: FileList): Promise<ParsedItem[]> {
|
async function parseFolders(files: FileList): Promise<ParsedItem[]> {
|
||||||
|
const fileByPath = buildFilePathMap(files)
|
||||||
|
const jsonFiles = Array.from(fileByPath.values()).filter(
|
||||||
|
(file, index, arr) =>
|
||||||
|
file.name.toLowerCase().endsWith('.json') && arr.indexOf(file) === index
|
||||||
|
)
|
||||||
|
for (const jsonFile of jsonFiles) {
|
||||||
|
try {
|
||||||
|
const configItems = await parseConfigJson(jsonFile, fileByPath)
|
||||||
|
if (configItems.length > 0) {
|
||||||
|
return configItems
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not the new array-based config. Fall back to legacy per-folder metadata.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 按第一层目录名分组
|
// 按第一层目录名分组
|
||||||
const groups = new Map<string, FileWithPath[]>()
|
const groups = new Map<string, FileWithPath[]>()
|
||||||
for (let i = 0; i < files.length; i += 1) {
|
for (let i = 0; i < files.length; i += 1) {
|
||||||
@@ -121,17 +236,17 @@ async function parseFolders(files: FileList): Promise<ParsedItem[]> {
|
|||||||
const plan = json.plan || {}
|
const plan = json.plan || {}
|
||||||
item.anchorText = truncate(plan.anchor_text, 500)
|
item.anchorText = truncate(plan.anchor_text, 500)
|
||||||
item.answer = (plan.answer || '').trim()
|
item.answer = (plan.answer || '').trim()
|
||||||
item.punchline = (plan.riddle?.homophone_explanation || '').trim()
|
item.punchline = (
|
||||||
|
plan.homophone_explanation ||
|
||||||
|
plan.riddle?.homophone_explanation ||
|
||||||
|
''
|
||||||
|
).trim()
|
||||||
const hints = Array.isArray(plan.hints) ? plan.hints : []
|
const hints = Array.isArray(plan.hints) ? plan.hints : []
|
||||||
item.hint1 = (hints[0] || '').trim()
|
item.hint1 = (hints[0] || '').trim()
|
||||||
item.hint2 = (hints[1] || '').trim()
|
item.hint2 = (hints[1] || '').trim()
|
||||||
item.hint3 = (hints[2] || '').trim()
|
item.hint3 = (hints[2] || '').trim()
|
||||||
|
|
||||||
if (!item.answer) {
|
validateItem(item)
|
||||||
item.parseError = '未解析到 answer'
|
|
||||||
} else if (item.answer.length > 4) {
|
|
||||||
item.parseError = `答案超过 4 字(${item.answer.length})`
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
item.parseError = `解析 metadata.json 失败: ${
|
item.parseError = `解析 metadata.json 失败: ${
|
||||||
e instanceof Error ? e.message : String(e)
|
e instanceof Error ? e.message : String(e)
|
||||||
@@ -198,7 +313,7 @@ export function BatchImportDialog({
|
|||||||
const parsed = await parseFolders(files)
|
const parsed = await parseFolders(files)
|
||||||
if (parsed.length === 0) {
|
if (parsed.length === 0) {
|
||||||
setGlobalError(
|
setGlobalError(
|
||||||
'未在所选目录中找到任何有效关卡。请确保每个子目录同时包含 metadata.json、reference.webp、riddle.webp'
|
'未在所选目录中找到任何有效关卡。请选择包含 riddles.json 和 riddle_images 的目录,或旧格式的 metadata.json、reference.webp、riddle.webp 子目录'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setItems(parsed)
|
setItems(parsed)
|
||||||
@@ -221,8 +336,8 @@ export function BatchImportDialog({
|
|||||||
// 每次编辑后重新评估答案的校验
|
// 每次编辑后重新评估答案的校验
|
||||||
if (patch.answer !== undefined) {
|
if (patch.answer !== undefined) {
|
||||||
if (!next.answer) next.parseError = '未填写答案'
|
if (!next.answer) next.parseError = '未填写答案'
|
||||||
else if (next.answer.length > 4)
|
else if (next.answer.length > ANSWER_MAX_LENGTH)
|
||||||
next.parseError = `答案超过 4 字(${next.answer.length})`
|
next.parseError = `答案超过 ${ANSWER_MAX_LENGTH} 字(${next.answer.length})`
|
||||||
else next.parseError = undefined
|
else next.parseError = undefined
|
||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
@@ -303,8 +418,7 @@ export function BatchImportDialog({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>批量导入关卡</DialogTitle>
|
<DialogTitle>批量导入关卡</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
选择包含多个关卡子文件夹的目录。每个子文件夹须含 metadata.json、
|
选择包含 riddles.json 和 riddle_images 的目录;也兼容旧格式的多个关卡子文件夹。
|
||||||
reference.webp、riddle.webp。
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -516,7 +630,7 @@ function ItemCard({ index, item, disabled, onChange, onRemove }: ItemCardProps)
|
|||||||
<Input
|
<Input
|
||||||
value={item.answer}
|
value={item.answer}
|
||||||
onChange={(e) => onChange({ answer: e.target.value })}
|
onChange={(e) => onChange({ answer: e.target.value })}
|
||||||
maxLength={4}
|
maxLength={ANSWER_MAX_LENGTH}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -226,12 +226,12 @@ export function LevelDialog({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData((prev) => ({ ...prev, answer: e.target.value }))
|
setFormData((prev) => ({ ...prev, answer: e.target.value }))
|
||||||
}
|
}
|
||||||
placeholder="请输入答案(最多4个字)"
|
placeholder="请输入答案(最多8个字)"
|
||||||
maxLength={4}
|
maxLength={8}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground text-right">
|
<p className="text-xs text-muted-foreground text-right">
|
||||||
{formData.answer.length}/4
|
{formData.answer.length}/8
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
128
package-lock.json
generated
128
package-lock.json
generated
@@ -8,21 +8,21 @@
|
|||||||
"name": "meme-studio",
|
"name": "meme-studio",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"better-auth": "^1.2.7",
|
"better-auth": "^1.2.7",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cos-js-sdk-v5": "^1.10.1",
|
"cos-js-sdk-v5": "^1.10.1",
|
||||||
"cos-nodejs-sdk-v5": "^2.14.0",
|
"cos-nodejs-sdk-v5": "^2.14.0",
|
||||||
|
"fractional-indexing": "^3.2.0",
|
||||||
"lucide-react": "^0.483.0",
|
"lucide-react": "^0.483.0",
|
||||||
"next": "14.2.28",
|
"next": "14.2.28",
|
||||||
"qcloud-cos-sts": "^3.1.1",
|
"qcloud-cos-sts": "^3.1.1",
|
||||||
@@ -73,59 +73,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
|
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
|
||||||
"integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
|
"integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
|
||||||
},
|
},
|
||||||
"node_modules/@dnd-kit/accessibility": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@dnd-kit/core": {
|
|
||||||
"version": "6.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
|
||||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-dom": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@dnd-kit/sortable": {
|
|
||||||
"version": "10.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
|
||||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@dnd-kit/core": "^6.3.0",
|
|
||||||
"react": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@dnd-kit/utilities": {
|
|
||||||
"version": "3.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
|
||||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||||
@@ -1657,6 +1604,66 @@
|
|||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-table": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.21.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.13.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz",
|
||||||
|
"integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.14.0"
|
||||||
|
},
|
||||||
|
"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/table-core": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz",
|
||||||
|
"integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@@ -4818,6 +4825,15 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fractional-indexing": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==",
|
||||||
|
"license": "CC0-1.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user