From b5bcc3d7a40215a85e6ea8866dfceb256aab5671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B3=E4=BD=A9=E5=BA=AD?= Date: Tue, 7 Oct 2025 12:34:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=AE=A1=E7=90=86=E8=80=85?= =?UTF-8?q?=E4=BB=8B=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 599 +++++++++++++++++++++++++++ app/api/admin/export-csv/route.ts | 73 ++++ app/api/admin/export-excel/route.ts | 84 ++++ app/api/admin/stats/route.ts | 107 +++++ app/api/admin/wishes/route.ts | 74 ++++ app/page.tsx | 8 +- package-lock.json | 614 ++++++++++++++++++++++++++++ package.json | 3 +- scripts/test-admin-export.js | 76 ++++ 9 files changed, 1636 insertions(+), 2 deletions(-) create mode 100644 app/admin/page.tsx create mode 100644 app/api/admin/export-csv/route.ts create mode 100644 app/api/admin/export-excel/route.ts create mode 100644 app/api/admin/stats/route.ts create mode 100644 app/api/admin/wishes/route.ts create mode 100644 scripts/test-admin-export.js diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..8d2bb9c --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,599 @@ +"use client" + +import { useState, useEffect } from "react" +import Link from "next/link" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { + Download, + Eye, + Users, + Heart, + FileText, + BarChart3, + RefreshCw, + Search, + Filter, + ArrowLeft, + Sparkles, + Settings, + Plus, + ChevronLeft, + ChevronRight +} from "lucide-react" +import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import HeaderMusicControl from "@/components/header-music-control" +import IpDisplay from "@/components/ip-display" + +interface WishData { + id: number + title: string + current_pain: string + expected_solution: string + expected_effect: string + is_public: boolean + email: string + images: any[] + user_session: string + status: string + category: string + priority: number + like_count: number + created_at: string + updated_at: string +} + +interface AdminStats { + totalWishes: number + publicWishes: number + privateWishes: number + totalLikes: number + categories: { [key: string]: number } + recentWishes: number +} + +export default function AdminPage() { + const [wishes, setWishes] = useState([]) + const [filteredWishes, setFilteredWishes] = useState([]) + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + const [searchTerm, setSearchTerm] = useState("") + const [statusFilter, setStatusFilter] = useState("all") + const [visibilityFilter, setVisibilityFilter] = useState("all") + const [isExporting, setIsExporting] = useState(false) + + // 分頁狀態 + const [currentPage, setCurrentPage] = useState(1) + const itemsPerPage = 10 + + // 獲取所有數據 + const fetchData = async () => { + try { + setLoading(true) + + // 獲取困擾案例數據 + const wishesResponse = await fetch('/api/admin/wishes') + const wishesResult = await wishesResponse.json() + + if (wishesResult.success) { + setWishes(wishesResult.data) + setFilteredWishes(wishesResult.data) + } + + // 獲取統計數據 + const statsResponse = await fetch('/api/admin/stats') + const statsResult = await statsResponse.json() + + if (statsResult.success) { + setStats(statsResult.data) + } + + } catch (error) { + console.error('獲取數據失敗:', error) + } finally { + setLoading(false) + } + } + + // 過濾數據 + const filterData = () => { + let filtered = wishes + + // 搜索過濾 + if (searchTerm) { + filtered = filtered.filter(wish => + wish.title.toLowerCase().includes(searchTerm.toLowerCase()) || + wish.current_pain.toLowerCase().includes(searchTerm.toLowerCase()) || + wish.expected_solution.toLowerCase().includes(searchTerm.toLowerCase()) + ) + } + + // 狀態過濾 + if (statusFilter !== "all") { + filtered = filtered.filter(wish => wish.status === statusFilter) + } + + // 可見性過濾 + if (visibilityFilter !== "all") { + filtered = filtered.filter(wish => + visibilityFilter === "public" ? wish.is_public : !wish.is_public + ) + } + + setFilteredWishes(filtered) + setCurrentPage(1) // 重置到第一頁 + } + + // 分頁計算 + const totalPages = Math.ceil(filteredWishes.length / itemsPerPage) + const startIndex = (currentPage - 1) * itemsPerPage + const endIndex = startIndex + itemsPerPage + const currentWishes = filteredWishes.slice(startIndex, endIndex) + + // 分頁組件 + const PaginationComponent = () => { + if (totalPages <= 1) return null + + const getPageNumbers = () => { + const pages = [] + const maxVisiblePages = 5 + + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i) + } + } else { + if (currentPage <= 3) { + for (let i = 1; i <= 4; i++) { + pages.push(i) + } + pages.push('...') + pages.push(totalPages) + } else if (currentPage >= totalPages - 2) { + pages.push(1) + pages.push('...') + for (let i = totalPages - 3; i <= totalPages; i++) { + pages.push(i) + } + } else { + pages.push(1) + pages.push('...') + for (let i = currentPage - 1; i <= currentPage + 1; i++) { + pages.push(i) + } + pages.push('...') + pages.push(totalPages) + } + } + + return pages + } + + return ( +
+
+ 顯示第 {startIndex + 1} - {Math.min(endIndex, filteredWishes.length)} 筆,共 {filteredWishes.length} 筆 +
+
+ + + {getPageNumbers().map((page, index) => ( + + ))} + + +
+
+ ) + } + + // 匯出 CSV + const exportToCSV = async () => { + try { + setIsExporting(true) + + const response = await fetch('/api/admin/export-csv', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: filteredWishes, + filename: `困擾案例數據_${new Date().toISOString().split('T')[0]}.csv` + }) + }) + + if (response.ok) { + const blob = await response.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `困擾案例數據_${new Date().toISOString().split('T')[0]}.csv` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + } else { + throw new Error('匯出失敗') + } + } catch (error) { + console.error('匯出 CSV 失敗:', error) + alert('匯出失敗,請稍後再試') + } finally { + setIsExporting(false) + } + } + + useEffect(() => { + fetchData() + }, []) + + useEffect(() => { + filterData() + }, [searchTerm, statusFilter, visibilityFilter, wishes]) + + if (loading) { + return ( +
+
+ +

載入中...

+
+
+ ) + } + + return ( +
+ {/* 星空背景 */} +
+
+
+ + {/* Header */} +
+
+
+ {/* Logo 區域 */} +
+
+ +
+

資訊部.心願星河

+
+ + {/* 導航區域 */} + +
+
+
+ + {/* 主要內容 */} +
+
+ {/* 標題區域 */} +
+
+ +

後台管理系統

+
+

困擾案例數據管理與分析

+
+ + {/* 統計卡片 */} + {stats && ( +
+ + + 總案例數 + + + +
{stats.totalWishes}
+

+ 公開 {stats.publicWishes} + 私密 {stats.privateWishes} +

+
+
+ + + + 總點讚數 + + + +
{stats.totalLikes}
+

用戶支持總數

+
+
+ + + + 問題領域 + + + +
{Object.keys(stats.categories).length}
+

不同類別

+
+
+ + + + 本週新增 + + + +
{stats.recentWishes}
+

最近7天

+
+
+
+ )} + + {/* 主要內容區域 */} + + + 數據管理 + 數據分析 + + + + {/* 搜索和過濾區域 */} + + + + + 數據篩選 + + + 搜索和過濾困擾案例數據 + + + +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 bg-slate-700/50 border-slate-600/50 text-white placeholder:text-blue-300 focus:border-cyan-400/50" + /> +
+ + + + + + +
+
+
+ + {/* 數據表格 */} + + + + + 困擾案例列表 + + + 共 {filteredWishes.length} 筆數據 + + + +
+ + + + + + + + + + + + + + {currentWishes.map((wish) => ( + + + + + + + + + + ))} + +
ID標題狀態可見性點讚數創建時間操作
{wish.id} + {wish.title} + + + {wish.status === 'active' ? '活躍' : '非活躍'} + + + + {wish.is_public ? '公開' : '私密'} + + + + {wish.like_count} + + {new Date(wish.created_at).toLocaleDateString('zh-TW')} + + +
+
+ + {/* 分頁組件 */} + +
+
+
+ + + + + + + 數據分析 + + + 困擾案例統計與分析 + + + +
+ {/* 類別分布 */} +
+

+ + 問題類別分布 +

+
+ {stats && Object.entries(stats.categories).map(([category, count]) => ( +
+ {category} + + {count} + +
+ ))} +
+
+ + {/* 時間分布 */} +
+

+ + 創建時間分布 +

+
+
+
{stats?.recentWishes}
+
最近7天新增
+
+
+
{stats?.totalWishes}
+
總案例數
+
+
+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/app/api/admin/export-csv/route.ts b/app/api/admin/export-csv/route.ts new file mode 100644 index 0000000..c80cedd --- /dev/null +++ b/app/api/admin/export-csv/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { data, filename } = body + + console.log(`🔍 後台管理 - 匯出 CSV: ${filename}`) + + // 準備 CSV 數據 + const headers = [ + 'ID', + '標題', + '遇到的困擾', + '期望的解決方式', + '預期效果', + '是否公開', + '電子郵件', + '狀態', + '類別', + '優先級', + '點讚數', + '創建時間', + '更新時間', + '用戶會話' + ] + + // 轉換數據為 CSV 格式 + const csvRows = [headers.join(',')] + + data.forEach((wish: any) => { + const row = [ + wish.id, + `"${wish.title.replace(/"/g, '""')}"`, + `"${wish.current_pain.replace(/"/g, '""')}"`, + `"${wish.expected_solution.replace(/"/g, '""')}"`, + `"${(wish.expected_effect || '').replace(/"/g, '""')}"`, + wish.is_public ? '是' : '否', + `"${(wish.email || '').replace(/"/g, '""')}"`, + wish.status === 'active' ? '活躍' : '非活躍', + `"${(wish.category || '未分類').replace(/"/g, '""')}"`, + wish.priority, + wish.like_count, + `"${new Date(wish.created_at).toLocaleString('zh-TW')}"`, + `"${new Date(wish.updated_at).toLocaleString('zh-TW')}"`, + `"${wish.user_session.replace(/"/g, '""')}"` + ] + csvRows.push(row.join(',')) + }) + + const csvContent = csvRows.join('\n') + const csvBuffer = Buffer.from(csvContent, 'utf-8') + + console.log(`✅ 成功生成 CSV 文件: ${data.length} 筆數據`) + + // 返回文件 + return new NextResponse(csvBuffer, { + status: 200, + headers: { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(filename)}"`, + 'Content-Length': csvBuffer.length.toString() + } + }) + + } catch (error) { + console.error('CSV 匯出錯誤:', error) + return NextResponse.json( + { success: false, error: 'Failed to export CSV' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/export-excel/route.ts b/app/api/admin/export-excel/route.ts new file mode 100644 index 0000000..2dcdde7 --- /dev/null +++ b/app/api/admin/export-excel/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { data, filename } = body + + console.log(`🔍 後台管理 - 匯出 Excel: ${filename}`) + + // 動態導入 xlsx + const XLSX = await import('xlsx') + + // 準備 Excel 數據 + const excelData = data.map((wish: any) => ({ + 'ID': wish.id, + '標題': wish.title, + '遇到的困擾': wish.current_pain, + '期望的解決方式': wish.expected_solution, + '預期效果': wish.expected_effect || '', + '是否公開': wish.is_public ? '是' : '否', + '電子郵件': wish.email || '', + '狀態': wish.status === 'active' ? '活躍' : '非活躍', + '類別': wish.category || '未分類', + '優先級': wish.priority, + '點讚數': wish.like_count, + '創建時間': new Date(wish.created_at).toLocaleString('zh-TW'), + '更新時間': new Date(wish.updated_at).toLocaleString('zh-TW'), + '用戶會話': wish.user_session + })) + + // 創建工作簿 + const workbook = XLSX.utils.book_new() + + // 創建工作表 + const worksheet = XLSX.utils.json_to_sheet(excelData) + + // 設置列寬 + const columnWidths = [ + { wch: 8 }, // ID + { wch: 30 }, // 標題 + { wch: 40 }, // 遇到的困擾 + { wch: 40 }, // 期望的解決方式 + { wch: 30 }, // 預期效果 + { wch: 10 }, // 是否公開 + { wch: 25 }, // 電子郵件 + { wch: 10 }, // 狀態 + { wch: 15 }, // 類別 + { wch: 8 }, // 優先級 + { wch: 8 }, // 點讚數 + { wch: 20 }, // 創建時間 + { wch: 20 }, // 更新時間 + { wch: 25 } // 用戶會話 + ] + worksheet['!cols'] = columnWidths + + // 添加工作表到工作簿 + XLSX.utils.book_append_sheet(workbook, worksheet, '困擾案例數據') + + // 生成 Excel 文件 + const excelBuffer = XLSX.write(workbook, { + type: 'buffer', + bookType: 'xlsx' + }) + + console.log(`✅ 成功生成 Excel 文件: ${excelData.length} 筆數據`) + + // 返回文件 + return new NextResponse(excelBuffer, { + status: 200, + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(filename)}"`, + 'Content-Length': excelBuffer.length.toString() + } + }) + + } catch (error) { + console.error('Excel 匯出錯誤:', error) + return NextResponse.json( + { success: false, error: 'Failed to export Excel' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/stats/route.ts b/app/api/admin/stats/route.ts new file mode 100644 index 0000000..e5dfc23 --- /dev/null +++ b/app/api/admin/stats/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from 'next/server' +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +export async function GET(request: NextRequest) { + try { + console.log('🔍 後台管理 - 獲取統計數據...') + + // 獲取基本統計 + const totalWishes = await prisma.wish.count({ + where: { status: 'active' } + }) + + const publicWishes = await prisma.wish.count({ + where: { + status: 'active', + isPublic: true + } + }) + + const privateWishes = totalWishes - publicWishes + + // 獲取總點讚數 + const totalLikes = await prisma.wishLike.count() + + // 獲取本週新增(最近7天) + const oneWeekAgo = new Date() + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) + + const recentWishes = await prisma.wish.count({ + where: { + status: 'active', + createdAt: { + gte: oneWeekAgo + } + } + }) + + // 獲取所有困擾案例進行自動分類 + const allWishes = await prisma.wish.findMany({ + where: { + status: 'active' + }, + select: { + id: true, + title: true, + currentPain: true, + expectedSolution: true, + category: true + } + }) + + // 導入分類函數 + const { categorizeWishMultiple } = await import('@/lib/categorization') + + // 對每個困擾案例進行分類 + const categories: { [key: string]: number } = {} + + allWishes.forEach(wish => { + // 如果資料庫中已有分類,使用資料庫的分類 + if (wish.category && wish.category !== 'NULL' && wish.category !== '') { + categories[wish.category] = (categories[wish.category] || 0) + 1 + } else { + // 否則進行自動分類 + const wishData = { + title: wish.title, + currentPain: wish.currentPain, + expectedSolution: wish.expectedSolution + } + + const categories_result = categorizeWishMultiple(wishData) + if (categories_result.length > 0) { + // 使用第一個分類的名稱,但排除「其他問題」 + const primaryCategory = categories_result[0].name + if (primaryCategory !== '其他問題') { + categories[primaryCategory] = (categories[primaryCategory] || 0) + 1 + } + } + // 注意:不統計「其他問題」和「未分類」的案例 + } + }) + + const stats = { + totalWishes, + publicWishes, + privateWishes, + totalLikes, + recentWishes, + categories + } + + console.log(`✅ 成功獲取統計數據: 總計 ${totalWishes} 個困擾案例`) + + return NextResponse.json({ + success: true, + data: stats + }) + + } catch (error) { + console.error('後台管理統計 API Error:', error) + return NextResponse.json( + { success: false, error: 'Failed to fetch admin stats' }, + { status: 500 } + ) + } +} diff --git a/app/api/admin/wishes/route.ts b/app/api/admin/wishes/route.ts new file mode 100644 index 0000000..5db3115 --- /dev/null +++ b/app/api/admin/wishes/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +export async function GET(request: NextRequest) { + try { + console.log('🔍 後台管理 - 獲取所有困擾案例數據...') + + // 獲取所有困擾案例(包含私密的) + const wishes = await prisma.$queryRaw` + SELECT id, title, current_pain, expected_solution, expected_effect, + is_public, email, images, user_session, status, category, priority, + created_at, updated_at + FROM wishes + WHERE status = 'active' + ORDER BY id DESC + LIMIT 1000 + ` + + // 獲取點讚數 + const wishIds = (wishes as any[]).map(w => w.id) + const likeCountMap = {} + + if (wishIds.length > 0) { + const likes = await prisma.wishLike.findMany({ + where: { + wishId: { in: wishIds } + }, + select: { + wishId: true + } + }) + + // 計算點讚數 + likes.forEach(like => { + likeCountMap[Number(like.wishId)] = (likeCountMap[Number(like.wishId)] || 0) + 1 + }) + } + + // 轉換數據格式 + const formattedWishes = (wishes as any[]).map((wish) => ({ + id: Number(wish.id), + title: wish.title, + current_pain: wish.current_pain, + expected_solution: wish.expected_solution, + expected_effect: wish.expected_effect, + is_public: Boolean(wish.is_public), + email: wish.email, + images: wish.images, + user_session: wish.user_session, + status: wish.status, + category: wish.category, + priority: Number(wish.priority), + like_count: likeCountMap[Number(wish.id)] || 0, + created_at: wish.created_at ? new Date(wish.created_at).toISOString() : new Date().toISOString(), + updated_at: wish.updated_at ? new Date(wish.updated_at).toISOString() : new Date().toISOString() + })) + + console.log(`✅ 成功獲取 ${formattedWishes.length} 筆困擾案例數據`) + + return NextResponse.json({ + success: true, + data: formattedWishes + }) + + } catch (error) { + console.error('後台管理 API Error:', error) + return NextResponse.json( + { success: false, error: 'Failed to fetch admin data' }, + { status: 500 } + ) + } +} diff --git a/app/page.tsx b/app/page.tsx index c06be60..f8b34cc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react" import Link from "next/link" import { Button } from "@/components/ui/button" -import { Sparkles, MessageCircle, Users, BarChart3 } from "lucide-react" +import { Sparkles, MessageCircle, Users, BarChart3, Settings } from "lucide-react" import HeaderMusicControl from "@/components/header-music-control" import IpDisplay from "@/components/ip-display" @@ -120,6 +120,12 @@ export default function HomePage() { 分享困擾 + + + {/* 平板版導航 */} diff --git a/package-lock.json b/package-lock.json index 67fadd8..308a8e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.9.1", + "@prisma/client": "^6.13.0", "@radix-ui/react-accordion": "1.2.2", "@radix-ui/react-alert-dialog": "1.1.4", "@radix-ui/react-aspect-ratio": "1.1.1", @@ -47,8 +48,10 @@ "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", "lucide-react": "^0.454.0", + "mysql2": "^3.14.3", "next": "14.2.16", "next-themes": "^0.4.4", + "prisma": "^6.13.0", "react": "^18", "react-day-picker": "8.10.1", "react-dom": "^18", @@ -61,6 +64,7 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.6", + "xlsx": "^0.18.5", "zod": "^3.24.1" }, "devDependencies": { @@ -1408,6 +1412,85 @@ "node": ">=14" } }, + "node_modules/@prisma/client": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.3.tgz", + "integrity": "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz", + "integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz", + "integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz", + "integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.3", + "@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", + "@prisma/fetch-engine": "6.16.3", + "@prisma/get-platform": "6.16.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz", + "integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz", + "integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.3", + "@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", + "@prisma/get-platform": "6.16.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz", + "integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.3" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -2724,6 +2807,12 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@supabase/auth-js": { "version": "2.71.1", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz", @@ -2988,6 +3077,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3162,6 +3260,15 @@ "postcss": "^8.1.0" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3329,6 +3436,83 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/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/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/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/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/c12/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/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/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3397,6 +3581,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -3434,6 +3631,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -3600,6 +3806,15 @@ "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", "license": "MIT" }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -3650,6 +3865,21 @@ "node": ">=14" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -3734,6 +3964,18 @@ } } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3939,6 +4181,15 @@ "node": ">=0.10.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/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/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -3951,6 +4202,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3960,6 +4226,12 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -4053,6 +4325,16 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.194", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", @@ -4094,6 +4376,15 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -4286,6 +4577,34 @@ "express": ">= 4.11" } }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4435,6 +4754,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4495,6 +4823,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4583,6 +4920,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4912,6 +5266,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regexp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", @@ -5111,6 +5471,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5132,6 +5498,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "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.454.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz", @@ -5341,6 +5722,42 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mysql2": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.1.tgz", + "integrity": "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==", + "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/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/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/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -5353,6 +5770,27 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5506,6 +5944,12 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -5558,6 +6002,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5589,6 +6052,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5761,6 +6230,18 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5808,6 +6289,17 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/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/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5956,6 +6448,31 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prisma": { + "version": "6.16.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz", + "integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.16.3", + "@prisma/engines": "6.16.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6029,6 +6546,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/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/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -6094,6 +6627,16 @@ "node": ">= 0.8" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/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": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -6559,6 +7102,11 @@ "node": ">= 18" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -6818,6 +7366,27 @@ "node": ">=0.10.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -7174,6 +7743,12 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7526,6 +8101,24 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -7672,6 +8265,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 6cdeb09..8e6c0ae 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.6", + "xlsx": "^0.18.5", "zod": "^3.24.1" }, "devDependencies": { @@ -75,4 +76,4 @@ "tailwindcss": "^3.4.17", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/scripts/test-admin-export.js b/scripts/test-admin-export.js new file mode 100644 index 0000000..ee8b958 --- /dev/null +++ b/scripts/test-admin-export.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +/** + * 測試後台管理匯出功能 + */ + +async function testAdminExport() { + try { + console.log('🔍 測試後台管理匯出功能...') + console.log('') + + // 1. 測試獲取數據 + console.log('1️⃣ 測試獲取困擾案例數據...') + const wishesResponse = await fetch('http://localhost:3000/api/admin/wishes') + const wishesResult = await wishesResponse.json() + + if (wishesResult.success) { + console.log(`✅ 成功獲取 ${wishesResult.data.length} 筆困擾案例數據`) + } else { + console.log(`❌ 獲取失敗: ${wishesResult.error}`) + return + } + console.log('') + + // 2. 測試獲取統計數據 + console.log('2️⃣ 測試獲取統計數據...') + const statsResponse = await fetch('http://localhost:3000/api/admin/stats') + const statsResult = await statsResponse.json() + + if (statsResult.success) { + console.log(`✅ 成功獲取統計數據:`) + console.log(` 總案例數: ${statsResult.data.totalWishes}`) + console.log(` 公開案例: ${statsResult.data.publicWishes}`) + console.log(` 私密案例: ${statsResult.data.privateWishes}`) + console.log(` 總點讚數: ${statsResult.data.totalLikes}`) + console.log(` 本週新增: ${statsResult.data.recentWishes}`) + } else { + console.log(`❌ 獲取統計失敗: ${statsResult.error}`) + } + console.log('') + + // 3. 測試 CSV 匯出 + console.log('3️⃣ 測試 CSV 匯出...') + const exportResponse = await fetch('http://localhost:3000/api/admin/export-csv', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: wishesResult.data.slice(0, 5), // 只匯出前5筆測試 + filename: 'test_export.csv' + }) + }) + + if (exportResponse.ok) { + const csvContent = await exportResponse.text() + console.log(`✅ CSV 匯出成功,內容長度: ${csvContent.length} 字元`) + console.log(` 前100字元: ${csvContent.substring(0, 100)}...`) + } else { + console.log(`❌ CSV 匯出失敗: ${exportResponse.status}`) + } + console.log('') + + console.log('🎉 後台管理功能測試完成!') + + } catch (error) { + console.error('❌ 測試失敗:', error.message) + } +} + +// 執行測試 +if (require.main === module) { + testAdminExport() +} + +module.exports = { testAdminExport }