diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/DEPLOYMENT-READY.md b/DEPLOYMENT-READY.md new file mode 100644 index 0000000..ab7e0fa --- /dev/null +++ b/DEPLOYMENT-READY.md @@ -0,0 +1,103 @@ +# 🚀 心願星河 - 佈署就緒確認 + +## ✅ 資料清理完成 + +### 已清理的項目 +- [x] 所有測試困擾案例 (wishes) +- [x] 點讚數據 (wishLikes) +- [x] 用戶點讚記錄 (userLikedWishes) +- [x] 背景音樂狀態 (backgroundMusicState) + +### 初始狀態設定 +- [x] 困擾案例:空陣列 `[]` +- [x] 點讚記錄:空物件 `{}` +- [x] 用戶記錄:空陣列 `[]` +- [x] 音樂設定:預設關閉狀態 + +## 🔍 佈署前最終檢查 + +### 功能驗證 +- [ ] 首頁載入正常,許願瓶動畫運作 +- [ ] 分享困擾頁面表單功能正常 +- [ ] 聆聽心聲頁面顯示空狀態提示 +- [ ] 問題洞察頁面顯示無資料狀態 +- [ ] 感謝頁面跳轉正常 +- [ ] 音效系統可正常開關 +- [ ] 背景音樂控制正常 +- [ ] 手機版隱私說明收放功能正常 + +### 響應式設計 +- [ ] 手機版 (320px-768px) 顯示正常 +- [ ] 平板版 (768px-1024px) 顯示正常 +- [ ] 桌面版 (1024px+) 顯示正常 +- [ ] 導航選單在各裝置正確顯示 +- [ ] 表單在各裝置易於操作 + +### 數據狀態 +- [ ] 所有統計數字顯示為 0 +- [ ] 分類統計圖表顯示無資料狀態 +- [ ] 搜尋和篩選功能正常 +- [ ] 隱私設定功能正常運作 + +## 🎯 佈署建議 + +### 推薦平台 +1. **Vercel** (推薦) + - 自動 CI/CD + - 優秀的 Next.js 支援 + - 免費 SSL 憑證 + - 全球 CDN + +2. **Netlify** + - 簡單易用 + - 自動佈署 + - 表單處理功能 + +3. **Vercel/Netlify 替代方案** + - GitHub Pages + - Firebase Hosting + +### 佈署步驟 (Vercel) +1. 將代碼推送到 GitHub +2. 連接 Vercel 到 GitHub 倉庫 +3. 設定建構命令:`npm run build` +4. 設定輸出目錄:`out` 或 `dist` +5. 執行佈署 + +### 環境變數 (如需要) +目前應用程式不需要特殊環境變數,但如果未來需要: +- `NEXT_PUBLIC_APP_URL`: 應用程式網址 +- `NEXT_PUBLIC_ANALYTICS_ID`: 分析追蹤 ID + +## 🌟 佈署後驗證 + +### 立即檢查 +1. 訪問所有頁面確認載入正常 +2. 提交一個測試困擾確認流程完整 +3. 檢查響應式設計在不同裝置 +4. 測試音效和背景音樂功能 +5. 驗證隱私設定功能 + +### 效能檢查 +- 頁面載入速度 < 3 秒 +- 動畫效果流暢 +- 音效載入不影響頁面效能 +- 圖片和資源正確載入 + +## 📞 技術支援 + +如遇到問題,請檢查: +1. 瀏覽器控制台是否有錯誤 +2. 網路連線是否正常 +3. 瀏覽器是否支援現代 JavaScript +4. 是否啟用了廣告攔截器 + +## 🎉 準備完成 + +**狀態**: ✅ 已準備好佈署 +**版本**: v1.0.0 +**清理時間**: ${new Date().toLocaleString('zh-TW')} + +--- + +**心願星河已準備好為用戶提供優質的職場困擾收集和分析服務!** 🌟 diff --git a/QUICK-START.md b/QUICK-START.md new file mode 100644 index 0000000..bd68c94 --- /dev/null +++ b/QUICK-START.md @@ -0,0 +1,55 @@ +# 🚀 心願星河 - 快速開始指南 + +## 📥 下載和設置 + +### 1. 下載專案 +- 點擊 v0 右上角的 **"Download Code"** 按鈕 +- 選擇下載方式並解壓縮 + +### 2. 安裝依賴 +\`\`\`bash +cd wish-pool +chmod +x setup-supabase.sh +./setup-supabase.sh +\`\`\` + +### 3. 創建 Supabase 項目 +1. 前往 [Supabase Dashboard](https://supabase.com/dashboard) +2. 點擊 "New Project" +3. 填寫項目資訊並等待創建完成 + +### 4. 配置環境變數 +編輯 `.env.local` 檔案: +\`\`\`env +NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here +\`\`\` + +### 5. 執行 SQL 腳本 +在 Supabase Dashboard 的 SQL Editor 中,按順序執行: +1. `scripts/01-create-tables.sql` +2. `scripts/02-create-indexes.sql` +3. `scripts/03-create-views-functions.sql` +4. `scripts/04-setup-storage.sql` +5. `scripts/05-setup-rls.sql` + +### 6. 測試連接 +\`\`\`bash +npm run test-supabase +\`\`\` + +### 7. 啟動應用 +\`\`\`bash +npm run dev +\`\`\` + +## 🎯 重要提醒 + +- ✅ 必須在本地環境執行,不能在 v0 中完成 +- ✅ 需要有 Supabase 帳號 +- ✅ 按順序執行 SQL 腳本很重要 +- ✅ 測試連接成功後再進行數據遷移 + +## 📞 需要幫助? + +參考完整文檔:`SUPABASE-COMPLETE-SETUP.md` diff --git a/SUPABASE-COMPLETE-SETUP.md b/SUPABASE-COMPLETE-SETUP.md new file mode 100644 index 0000000..d95009b --- /dev/null +++ b/SUPABASE-COMPLETE-SETUP.md @@ -0,0 +1,369 @@ +# 🚀 心願星河 - Supabase 完整建置指南 + +## 📋 目錄 +- [前置準備](#前置準備) +- [Supabase 項目設置](#supabase-項目設置) +- [本地環境配置](#本地環境配置) +- [數據庫建置](#數據庫建置) +- [存儲服務設置](#存儲服務設置) +- [安全政策配置](#安全政策配置) +- [測試驗證](#測試驗證) +- [數據遷移](#數據遷移) +- [部署準備](#部署準備) +- [故障排除](#故障排除) + +--- + +## 🎯 前置準備 + +### 系統需求 +- Node.js 18+ +- npm 或 yarn +- 現代瀏覽器 +- Supabase 帳號 + +### 預估時間 +- 初次設置:30-45 分鐘 +- 數據遷移:5-10 分鐘 +- 測試驗證:10-15 分鐘 + +--- + +## 🏗️ Supabase 項目設置 + +### 1. 創建新項目 +1. 前往 [Supabase Dashboard](https://supabase.com/dashboard) +2. 點擊 **"New Project"** +3. 填寫項目資訊: + \`\`\` + Name: wish-pool-production + Organization: [選擇你的組織] + Database Password: [設置強密碼,請記住!] + Region: [選擇最近的區域,建議 ap-southeast-1] + \`\`\` +4. 點擊 **"Create new project"** +5. 等待 2-3 分鐘完成初始化 + +### 2. 獲取項目配置 +項目創建完成後: +1. 進入項目 Dashboard +2. 左側選單 → **Settings** → **API** +3. 複製以下資訊: + \`\`\` + Project URL: https://[your-project-id].supabase.co + anon public key: eyJ... (很長的字串) + service_role key: eyJ... (僅在需要管理員功能時使用) + \`\`\` + +--- + +## ⚙️ 本地環境配置 + +### 1. 安裝依賴 +\`\`\`bash +# 安裝 Supabase 客戶端 +npm install @supabase/supabase-js + +# 如果需要圖片處理功能 +npm install sharp +\`\`\` + +### 2. 環境變數設置 +1. 複製環境變數範本: + \`\`\`bash + cp .env.local.example .env.local + \`\`\` + +2. 編輯 `.env.local`: + \`\`\`env + # Supabase 配置 + NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co + NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here + + # 可選:管理員功能(謹慎使用) + SUPABASE_SERVICE_ROLE_KEY=your-service-role-key + + # 應用配置 + NEXT_PUBLIC_APP_URL=http://localhost:3000 + \`\`\` + +### 3. 驗證連接 +\`\`\`bash +# 啟動開發服務器 +npm run dev + +# 檢查控制台是否有 Supabase 連接錯誤 +\`\`\` + +--- + +## 🗄️ 數據庫建置 + +### 執行順序很重要!請按照以下順序執行 SQL 腳本: + +### 步驟 1: 創建基礎表格 +1. 進入 Supabase Dashboard +2. 左側選單 → **SQL Editor** +3. 點擊 **"New Query"** +4. 複製並執行 `scripts/01-create-tables.sql` +5. 確認執行成功(無錯誤訊息) + +### 步驟 2: 創建索引和觸發器 +1. 新建查詢 +2. 複製並執行 `scripts/02-create-indexes.sql` +3. 確認所有索引創建成功 + +### 步驟 3: 創建視圖和函數 +1. 新建查詢 +2. 複製並執行 `scripts/03-create-views-functions.sql` +3. 確認視圖和函數創建成功 + +### 步驟 4: 設置存儲服務 +1. 新建查詢 +2. 複製並執行 `scripts/04-setup-storage.sql` +3. 檢查 Storage → Buckets 是否出現新的桶 + +### 步驟 5: 配置安全政策 +1. 新建查詢 +2. 複製並執行 `scripts/05-setup-rls.sql` +3. 確認 RLS 政策設置完成 + +--- + +## 📁 存儲服務設置 + +### 存儲桶說明 +- **wish-images**: 主要圖片存儲(5MB 限制) +- **wish-thumbnails**: 縮圖存儲(1MB 限制) + +### 支援格式 +- JPEG, JPG, PNG, WebP, GIF +- 自動壓縮和優化 +- CDN 加速分發 + +### 存儲政策 +- 公開讀取:所有人可查看圖片 +- 限制上傳:防止濫用 +- 自動清理:定期清理孤立圖片 + +--- + +## 🔒 安全政策配置 + +### Row Level Security (RLS) +所有表格都啟用了 RLS,確保數據安全: + +#### wishes 表格 +- ✅ 公開困擾案例:所有人可查看 +- ✅ 私密困擾案例:僅統計使用 +- ✅ 插入權限:所有人可提交 + +#### wish_likes 表格 +- ✅ 查看權限:用於統計顯示 +- ✅ 插入權限:所有人可點讚 +- ✅ 防重複:同一用戶不可重複點讚 + +#### user_settings 表格 +- ✅ 個人設定:用戶只能管理自己的設定 +- ✅ 會話隔離:基於 session ID 區分用戶 + +--- + +## 🧪 測試驗證 + +### 1. 數據庫連接測試 +\`\`\`sql +-- 在 SQL Editor 中執行 +SELECT 'Database connection successful!' as status; +\`\`\` + +### 2. 表格結構驗證 +\`\`\`sql +-- 檢查所有表格是否存在 +SELECT table_name +FROM information_schema.tables +WHERE table_schema = 'public' +ORDER BY table_name; +\`\`\` + +### 3. 存儲服務測試 +1. 進入 Storage → wish-images +2. 嘗試上傳一張測試圖片 +3. 確認可以正常預覽 + +### 4. 功能測試清單 +- [ ] 提交新的困擾案例 +- [ ] 上傳圖片附件 +- [ ] 點讚功能 +- [ ] 查看統計數據 +- [ ] 背景音樂設定保存 +- [ ] 響應式設計正常 + +--- + +## 🔄 數據遷移 + +### 自動遷移流程 +1. 啟動應用程式 +2. 如果檢測到本地數據,會自動顯示遷移對話框 +3. 點擊 **"開始遷移"** +4. 等待遷移完成 +5. 驗證數據完整性 + +### 手動遷移(如需要) +\`\`\`javascript +// 在瀏覽器控制台執行 +console.log('Local wishes:', JSON.parse(localStorage.getItem('wishes') || '[]')); +console.log('Local likes:', JSON.parse(localStorage.getItem('wishLikes') || '{}')); +\`\`\` + +### 遷移後清理 +\`\`\`javascript +// 確認遷移成功後,清理本地數據 +localStorage.removeItem('wishes'); +localStorage.removeItem('wishLikes'); +localStorage.removeItem('userLikedWishes'); +\`\`\` + +--- + +## 🚀 部署準備 + +### 1. 環境變數設置 +在部署平台(Vercel/Netlify)設置: +\`\`\` +NEXT_PUBLIC_SUPABASE_URL=your-production-url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-production-key +\`\`\` + +### 2. 生產環境優化 +\`\`\`sql +-- 執行生產環境優化 +SELECT optimize_database_performance(); +\`\`\` + +### 3. 備份設置 +1. 進入 Supabase Dashboard +2. Settings → Database → Backups +3. 確認自動備份已啟用 + +--- + +## 🔧 故障排除 + +### 常見問題 + +#### 1. 連接失敗 +**症狀**: `Failed to connect to Supabase` +**解決方案**: +- 檢查環境變數是否正確 +- 確認 Supabase 項目狀態正常 +- 檢查網路連接 + +#### 2. RLS 政策錯誤 +**症狀**: `Row Level Security policy violation` +**解決方案**: +\`\`\`sql +-- 檢查 RLS 政策 +SELECT * FROM pg_policies WHERE tablename = 'wishes'; +\`\`\` + +#### 3. 存儲上傳失敗 +**症狀**: 圖片上傳失敗 +**解決方案**: +- 檢查檔案大小(<5MB) +- 確認檔案格式支援 +- 檢查存儲桶政策 + +#### 4. 性能問題 +**症狀**: 查詢速度慢 +**解決方案**: +\`\`\`sql +-- 檢查索引使用情況 +EXPLAIN ANALYZE SELECT * FROM wishes_with_likes LIMIT 10; +\`\`\` + +### 日誌檢查 +1. Supabase Dashboard → Logs +2. 查看 Database、API、Storage 日誌 +3. 過濾錯誤和警告訊息 + +### 性能監控 +\`\`\`sql +-- 檢查數據庫性能 +SELECT * FROM get_performance_stats(); +\`\`\` + +--- + +## 📊 維護建議 + +### 定期維護任務 + +#### 每週 +- [ ] 檢查錯誤日誌 +- [ ] 監控存儲使用量 +- [ ] 清理孤立圖片 + +#### 每月 +- [ ] 分析查詢性能 +- [ ] 檢查備份完整性 +- [ ] 更新統計數據 + +#### 每季 +- [ ] 檢查安全政策 +- [ ] 優化數據庫索引 +- [ ] 評估擴展需求 + +### 清理腳本 +\`\`\`sql +-- 清理 30 天前的孤立圖片 +SELECT cleanup_orphaned_images(); + +-- 更新統計數據 +REFRESH MATERIALIZED VIEW wishes_stats_cache; +\`\`\` + +--- + +## 🎉 完成檢查清單 + +設置完成後,請確認以下項目: + +### 基礎設置 +- [ ] Supabase 項目創建成功 +- [ ] 環境變數配置正確 +- [ ] 所有 SQL 腳本執行成功 +- [ ] 存儲桶創建完成 + +### 功能測試 +- [ ] 可以提交新困擾案例 +- [ ] 圖片上傳功能正常 +- [ ] 點讚功能運作正常 +- [ ] 統計數據顯示正確 +- [ ] 用戶設定保存成功 + +### 安全檢查 +- [ ] RLS 政策生效 +- [ ] 無法訪問他人私密數據 +- [ ] 存儲權限設置正確 + +### 性能驗證 +- [ ] 頁面載入速度正常 +- [ ] 圖片載入速度快 +- [ ] 查詢響應時間合理 + +--- + +## 📞 支援資源 + +- **Supabase 官方文檔**: https://supabase.com/docs +- **Next.js 整合指南**: https://supabase.com/docs/guides/getting-started/quickstarts/nextjs +- **故障排除指南**: https://supabase.com/docs/guides/platform/troubleshooting + +--- + +**🌟 恭喜!你的心願星河已經成功整合 Supabase!** + +現在你可以享受雲端數據存儲、圖片管理、實時同步等強大功能。如果遇到任何問題,請參考故障排除章節或聯繫技術支援。 +\`\`\` diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index 62ca4c8..7eea8a6 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -24,6 +24,7 @@ import { import RadarChart from "@/components/radar-chart" import HeaderMusicControl from "@/components/header-music-control" import { categories, categorizeWishMultiple, type Wish } from "@/lib/categorization" +import { WishService } from "@/lib/supabase-service" interface CategoryData { name: string @@ -63,6 +64,7 @@ export default function AnalyticsPage() { const [wishes, setWishes] = useState([]) const [analytics, setAnalytics] = useState(null) const [showCategoryGuide, setShowCategoryGuide] = useState(false) + const [showPrivacyDetails, setShowPrivacyDetails] = useState(false) // 新增:隱私說明收放狀態 // 分析許願內容(包含所有數據,包括私密的) const analyzeWishes = (wishList: (Wish & { isPublic?: boolean })[]): AnalyticsData => { @@ -181,9 +183,39 @@ export default function AnalyticsPage() { } useEffect(() => { - const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]") - setWishes(savedWishes) - setAnalytics(analyzeWishes(savedWishes)) + const fetchWishes = async () => { + try { + // 獲取所有困擾案例(包含私密的,用於完整分析) + const allWishesData = await WishService.getAllWishes() + + // 轉換數據格式以匹配 categorization.ts 的 Wish 接口 + const convertWish = (wish: any) => ({ + id: wish.id, + title: wish.title, + currentPain: wish.current_pain, + expectedSolution: wish.expected_solution, + expectedEffect: wish.expected_effect || "", + createdAt: wish.created_at, + isPublic: wish.is_public, + email: wish.email, + images: wish.images, + like_count: wish.like_count || 0, // 包含點讚數 + }) + + const allWishes = allWishesData.map(convertWish) + + setWishes(allWishes) + setAnalytics(analyzeWishes(allWishes)) + } catch (error) { + console.error("獲取分析數據失敗:", error) + // 如果 Supabase 連接失敗,回退到 localStorage + const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]") + setWishes(savedWishes) + setAnalytics(analyzeWishes(savedWishes)) + } + } + + fetchWishes() }, []) if (!analytics) { @@ -320,71 +352,92 @@ export default function AnalyticsPage() { {/* Main Content */} -
+
- {/* 頁面標題 */} -
-
-
- + {/* 頁面標題 - 手機優化 */} +
+
+
+
-

問題洞察分析

- +

問題洞察分析

+ 完整數據分析
-

深入分析職場困擾的分布和趨勢

-

包含所有提交的案例數據,協助管理者了解真實狀況

+

深入分析職場困擾的分布和趨勢

+

包含所有提交的案例數據,協助管理者了解真實狀況

- {/* 隱私說明卡片 */} - - - -
- + {/* 隱私說明卡片 - 手機版可收放 */} + + +
+
+
+ +
+
+ 數據隱私說明 + + 本分析包含所有提交的案例,包括選擇保持私密的困擾 + +
- 數據隱私說明 - - - 本分析包含所有提交的案例,包括選擇保持私密的困擾 - + {/* 手機版收放按鈕 */} + +
- -
-
-

- - 公開案例 ({analytics.publicWishes} 個) -

-

這些案例會顯示在「聆聽心聲」頁面,供其他人查看和產生共鳴

+ + {/* 桌面版始終顯示,手機版可收放 */} +
+ +
+
+

+ + 公開案例 ({analytics.publicWishes} 個) +

+

+ 這些案例會顯示在「聆聽心聲」頁面,供其他人查看和產生共鳴 +

+
+
+

+ + 私密案例 ({analytics.privateWishes} 個) +

+

+ 這些案例保持匿名且私密,僅用於統計分析,幫助了解整體趨勢 +

+
-
-

- - 私密案例 ({analytics.privateWishes} 個) -

-

這些案例保持匿名且私密,僅用於統計分析,幫助了解整體趨勢

+
+

+ 給管理者的說明: + 此分析包含完整的問題數據,能幫助您了解團隊面臨的真實挑戰。私密案例雖然不會公開顯示, + 但其數據對於制定改善策略同樣重要。所有個人身份資訊都已匿名化處理。 +

-
-
-

- 給管理者的說明: - 此分析包含完整的問題數據,能幫助您了解團隊面臨的真實挑戰。私密案例雖然不會公開顯示, - 但其數據對於制定改善策略同樣重要。所有個人身份資訊都已匿名化處理。 -

-
-
+ +
- {/* 統計概覽 */} -
+ {/* 統計概覽 - 手機優化 */} +
- -
- + +
+
-
{analytics.totalWishes}
+
{analytics.totalWishes}
總案例數
公開 {analytics.publicWishes} + 私密 {analytics.privateWishes} @@ -393,21 +446,21 @@ export default function AnalyticsPage() { - -
- + +
+
-
{analytics.recentTrends.thisWeek}
+
{analytics.recentTrends.thisWeek}
本週新增
- -
- + +
+
-
+
{analytics.categories.filter((c) => c.count > 0).length}
問題領域
@@ -415,16 +468,16 @@ export default function AnalyticsPage() { - +
- +
-
+
{analytics.recentTrends.growth > 0 ? "+" : ""} {analytics.recentTrends.growth}%
@@ -437,17 +490,17 @@ export default function AnalyticsPage() {
- {/* 分類指南 */} - + {/* 分類指南 - 手機優化 */} +
-
-
- -
+
+
+ +
- 問題分類說明 - + 問題分類說明 + 了解我們如何分類和分析各種職場困擾
@@ -475,32 +528,35 @@ export default function AnalyticsPage() { {showCategoryGuide && ( -
+
{categories.map((category, index) => (
-
-
{category.icon}
-
+
+
{category.icon}
+
-

{category.name}

-
+

{category.name}

+
-

{category.description}

+

{category.description}

{/* 關鍵字示例 */} -
+
常見關鍵字:
{category.keywords.slice(0, 6).map((keyword, idx) => ( {keyword} @@ -508,7 +564,7 @@ export default function AnalyticsPage() { {category.keywords.length > 6 && ( +{category.keywords.length - 6} @@ -523,21 +579,23 @@ export default function AnalyticsPage() { {/* 手機版:垂直佈局,桌面版:並排佈局 */} -
+
{/* 雷達圖 - 手機版給予更多高度 */} - -
- + +
+
問題分布圖譜
- 各類職場困擾的完整案例分布(包含私密數據) + + 各類職場困擾的完整案例分布(包含私密數據) + {/* 手機版使用更大的高度,桌面版保持原有高度 */} -
+
@@ -546,16 +604,16 @@ export default function AnalyticsPage() { {/* 分類詳細統計 */} - -
- + +
+
完整案例統計 - + 含私密數據
- + 每個領域的所有案例數量(包含公開和私密案例) {analytics.categories.filter((cat) => cat.count > 0).length > 0 && ( @@ -565,39 +623,42 @@ export default function AnalyticsPage() { )} - + {/* 設定固定高度並添加滾動 */} -
+
{analytics.categories .filter((cat) => cat.count > 0) .sort((a, b) => b.count - a.count) .map((category, index) => (
-
-
+
+
{categories.find((cat) => cat.name === category.name)?.icon || "❓"}
-
-
- {category.name} -
+
+
+ {category.name} +
{/* 添加排名標示 */} {index < 3 && ( - + TOP {index + 1} )}
-
{category.count} 個案例
+
{category.count} 個案例
{category.description && ( -
{category.description}
+
{category.description}
)}
- + {category.percentage}%
@@ -622,33 +683,41 @@ export default function AnalyticsPage() {
{/* 多維度分析說明 */} - + - -
- + +
+
完整數據分析優勢
- 包含私密案例的全面分析,提供更準確的洞察 + + 包含私密案例的全面分析,提供更準確的洞察 + -
+

🔍 真實全貌

-

包含所有案例數據,不受公開意願影響,呈現最真實的問題狀況

+

+ 包含所有案例數據,不受公開意願影響,呈現最真實的問題狀況 +

📊 精準決策

-

基於完整數據制定改善策略,避免因樣本偏差導致的決策失誤

+

+ 基於完整數據制定改善策略,避免因樣本偏差導致的決策失誤 +

🎯 隱藏問題

-

發現那些員工不願公開但確實存在的問題,提前預防和解決

+

+ 發現那些員工不願公開但確實存在的問題,提前預防和解決 +

🔒 隱私保護

-

在保護個人隱私的前提下,最大化數據的分析價值

+

在保護個人隱私的前提下,最大化數據的分析價值

@@ -656,15 +725,15 @@ export default function AnalyticsPage() { {/* 熱門關鍵字 */} {analytics.topKeywords.length > 0 && ( - + - -
- + +
+
最常見的問題關鍵字
- + 在所有案例中最常出現的詞彙,反映團隊面臨的核心挑戰 @@ -674,7 +743,7 @@ export default function AnalyticsPage() { {keyword.word} ({keyword.count}) diff --git a/app/page.tsx b/app/page.tsx index 8959a92..d044a53 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,40 +1,72 @@ "use client" +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 HeaderMusicControl from "@/components/header-music-control" +interface Star { + id: number; + style: { + left: string; + top: string; + animationDelay: string; + animationDuration: string; + }; +} + export default function HomePage() { + const [stars, setStars] = useState([]); + const [bigStars, setBigStars] = useState([]); + + useEffect(() => { + // 生成小星星 + setStars( + Array.from({ length: 30 }, (_, i) => ({ + id: i, + style: { + left: `${Math.random() * 100}%`, + top: `${Math.random() * 100}%`, + animationDelay: `${Math.random() * 3}s`, + animationDuration: `${2 + Math.random() * 2}s`, + }, + })) + ); + + // 生成大星星 + setBigStars( + Array.from({ length: 15 }, (_, i) => ({ + id: i, + style: { + left: `${Math.random() * 100}%`, + top: `${Math.random() * 100}%`, + animationDelay: `${Math.random() * 4}s`, + animationDuration: `${3 + Math.random() * 2}s`, + }, + })) + ); + }, []); + return (
{/* 星空背景 */}
{/* 星星 */} - {[...Array(30)].map((_, i) => ( + {stars.map((star) => (
))} {/* 較大的星星 */} - {[...Array(15)].map((_, i) => ( + {bigStars.map((star) => (
))} diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000..f242703 --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,263 @@ +"use client" + +import { useState, useEffect } from "react" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Sparkles, ArrowLeft, Database, Settings, TestTube, Trash2 } from "lucide-react" +import HeaderMusicControl from "@/components/header-music-control" +import MigrationDialog from "@/components/migration-dialog" +import { testSupabaseConnection, MigrationService } from "@/lib/supabase-service" + +export default function SettingsPage() { + const [showMigration, setShowMigration] = useState(false) + const [isConnected, setIsConnected] = useState(false) + const [localDataCount, setLocalDataCount] = useState(0) + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + checkLocalData() + checkConnection() + }, []) + + const checkLocalData = () => { + try { + const wishes = JSON.parse(localStorage.getItem("wishes") || "[]") + setLocalDataCount(wishes.length) + } catch (error) { + setLocalDataCount(0) + } + } + + const checkConnection = async () => { + setIsLoading(true) + try { + const connected = await testSupabaseConnection() + setIsConnected(connected) + } catch (error) { + setIsConnected(false) + } finally { + setIsLoading(false) + } + } + + const clearAllData = () => { + if (confirm("確定要清除所有本地數據嗎?此操作無法復原。")) { + MigrationService.clearLocalStorageData() + // 也清除其他設定 + localStorage.removeItem("backgroundMusicState") + localStorage.removeItem("user_session") + setLocalDataCount(0) + alert("本地數據已清除") + } + } + + return ( +
+ {/* 星空背景 */} +
+ {[...Array(25)].map((_, i) => ( +
+ ))} +
+
+ + {/* Header */} +
+
+
+ +
+ +
+

心願星河

+ + + +
+
+
+ + {/* Main Content */} +
+
+
+

+ + 系統設定 +

+

管理數據存儲和系統配置

+
+ +
+ {/* Supabase 連接狀態 */} + + + + + Supabase 數據庫狀態 + + 雲端數據庫連接和配置狀態 + + +
+
+
+ 連接狀態 +
+
+ + {isLoading ? "檢查中..." : isConnected ? "已連接" : "未連接"} + + +
+
+ + {!isConnected && ( +
+

無法連接到 Supabase。請檢查:

+
    +
  • • 環境變數 NEXT_PUBLIC_SUPABASE_URL 是否正確
  • +
  • • 環境變數 NEXT_PUBLIC_SUPABASE_ANON_KEY 是否正確
  • +
  • • Supabase 項目是否正常運行
  • +
  • • 網路連接是否正常
  • +
+
+ )} +
+
+ + {/* 數據遷移 */} + {localDataCount > 0 && ( + + + + + 數據遷移 + 需要處理 + + + 發現 {localDataCount} 個本地困擾案例,建議遷移到雲端數據庫 + + + + + + + )} + + {/* 數據管理 */} + + + + + 數據管理 + + 清除本地存儲的數據 + + +
+
+ 本地困擾案例 + {localDataCount} 個 +
+

存儲在瀏覽器本地的困擾案例數據

+
+ + +
+
+ + {/* 系統資訊 */} + + + 系統資訊 + + +
+
+ 版本 +
v1.0.0
+
+
+ 數據庫 +
{isConnected ? "Supabase" : "LocalStorage"}
+
+
+ 用戶會話 +
+ {typeof window !== "undefined" + ? localStorage.getItem("user_session")?.slice(-8) || "未設定" + : "載入中..."} +
+
+
+ 瀏覽器 +
+ {typeof window !== "undefined" ? navigator.userAgent.split(" ").pop() : "未知"} +
+
+
+
+
+
+
+
+ + {/* 遷移對話框 */} + {showMigration && ( +
+
+ { + setShowMigration(false) + checkLocalData() + }} + onSkip={() => setShowMigration(false)} + /> +
+
+ )} +
+ ) +} diff --git a/app/settings/storage-management.tsx b/app/settings/storage-management.tsx new file mode 100644 index 0000000..fc9797a --- /dev/null +++ b/app/settings/storage-management.tsx @@ -0,0 +1,219 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Cloud, Trash2, RefreshCw, CheckCircle, AlertTriangle, HardDrive } from "lucide-react" +import { StorageHealthService } from "@/lib/supabase-service-updated" + +export default function StorageManagement() { + const [storageHealth, setStorageHealth] = useState<{ + healthy: boolean + stats?: { totalFiles: number; totalSize: number } + error?: string + } | null>(null) + const [isLoading, setIsLoading] = useState(false) + const [cleanupResult, setCleanupResult] = useState<{ cleaned: number; error?: string } | null>(null) + + useEffect(() => { + checkStorageHealth() + }, []) + + const checkStorageHealth = async () => { + setIsLoading(true) + try { + const health = await StorageHealthService.checkStorageHealth() + setStorageHealth(health) + } catch (error) { + setStorageHealth({ healthy: false, error: `檢查失敗: ${error}` }) + } finally { + setIsLoading(false) + } + } + + const cleanupOrphanedImages = async () => { + if (!confirm("確定要清理孤立的圖片嗎?這將刪除沒有被任何困擾案例引用的圖片。")) { + return + } + + setIsLoading(true) + try { + const result = await StorageHealthService.cleanupOrphanedImages() + setCleanupResult(result) + // 重新檢查存儲狀態 + await checkStorageHealth() + } catch (error) { + setCleanupResult({ cleaned: 0, error: `清理失敗: ${error}` }) + } finally { + setIsLoading(false) + } + } + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] + } + + return ( +
+ {/* 存儲狀態 */} + + + + + Supabase Storage 狀態 + + 雲端圖片存儲服務狀態和使用情況 + + +
+
+
+ 存儲服務狀態 +
+
+ + {isLoading ? "檢查中..." : storageHealth?.healthy ? "正常運行" : "服務異常"} + + +
+
+ + {/* 存儲統計 */} + {storageHealth?.stats && ( +
+
+
{storageHealth.stats.totalFiles}
+
圖片檔案
+
+
+
+ {formatFileSize(storageHealth.stats.totalSize)} +
+
總使用空間
+
+
+ )} + + {/* 存儲使用進度條 */} + {storageHealth?.stats && ( +
+
+ 存儲使用量 + {formatFileSize(storageHealth.stats.totalSize)} / 1GB (免費額度) +
+ +
+ )} + + {/* 錯誤訊息 */} + {storageHealth?.error && ( + + + +
+

存儲服務檢查失敗:

+

{storageHealth.error}

+
+
+
+ )} +
+
+ + {/* 存儲管理 */} + + + + + 存儲管理 + + 管理和優化雲端圖片存儲 + + +
+

+ + 清理孤立圖片 +

+

清理沒有被任何困擾案例引用的圖片檔案,釋放存儲空間。

+ +
+ + {/* 清理結果 */} + {cleanupResult && ( + + {cleanupResult.error ? ( + + ) : ( + + )} + + {cleanupResult.error ? ( +
+

清理過程中發生錯誤:

+

{cleanupResult.error}

+
+ ) : ( +
+

清理完成!

+

+ {cleanupResult.cleaned > 0 + ? `成功清理了 ${cleanupResult.cleaned} 個孤立的圖片檔案` + : "沒有發現需要清理的孤立圖片"} +

+
+ )} +
+
+ )} + + {/* 存儲最佳實踐 */} +
+

💡 存儲最佳實踐

+
    +
  • • 定期清理孤立圖片以節省存儲空間
  • +
  • • 上傳前壓縮大圖片以減少存儲使用量
  • +
  • • 避免上傳重複的圖片內容
  • +
  • • 使用適當的圖片格式(WebP 通常最優)
  • +
+
+
+
+
+ ) +} diff --git a/app/submit/page.tsx b/app/submit/page.tsx index a29dc3b..fe408eb 100644 --- a/app/submit/page.tsx +++ b/app/submit/page.tsx @@ -11,10 +11,15 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Checkbox } from "@/components/ui/checkbox" -import { Sparkles, ArrowLeft, Send, BarChart3, Eye, EyeOff, Shield, Info } from "lucide-react" +import { Sparkles, ArrowLeft, Send, BarChart3, Eye, EyeOff, Shield, Info, Mail, ImageIcon } from "lucide-react" import { useToast } from "@/hooks/use-toast" import { soundManager } from "@/lib/sound-effects" import HeaderMusicControl from "@/components/header-music-control" +import { moderateWishForm, type ModerationResult } from "@/lib/content-moderation" +import ContentModerationFeedback from "@/components/content-moderation-feedback" +import ImageUpload from "@/components/image-upload" +import type { ImageFile } from "@/lib/image-utils" +import { WishService } from "@/lib/supabase-service" export default function SubmitPage() { const [formData, setFormData] = useState({ @@ -22,11 +27,15 @@ export default function SubmitPage() { currentPain: "", expectedSolution: "", expectedEffect: "", - isPublic: true, // 預設為公開 + isPublic: true, + email: "", }) + const [images, setImages] = useState([]) const [isSubmitting, setIsSubmitting] = useState(false) const { toast } = useToast() const router = useRouter() + const [moderationResult, setModerationResult] = useState(null) + const [showModerationFeedback, setShowModerationFeedback] = useState(false) // 初始化音效系統 useEffect(() => { @@ -43,31 +52,64 @@ export default function SubmitPage() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + + // 先進行內容審核 + const moderation = moderateWishForm(formData) + setModerationResult(moderation) + + if (!moderation.isAppropriate) { + setShowModerationFeedback(true) + await soundManager.play("click") // 播放提示音效 + toast({ + title: "內容需要修改", + description: "請根據建議修改內容後再次提交", + variant: "destructive", + }) + return + } + setIsSubmitting(true) + setShowModerationFeedback(false) // 播放提交音效 await soundManager.play("submit") - await new Promise((resolve) => setTimeout(resolve, 1500)) + try { + // 創建困擾案例到 Supabase 數據庫 + await WishService.createWish({ + title: formData.title, + currentPain: formData.currentPain, + expectedSolution: formData.expectedSolution, + expectedEffect: formData.expectedEffect, + isPublic: formData.isPublic, + email: formData.email, + images: images, // 直接傳遞 ImageFile 數組 + }) - const wishes = JSON.parse(localStorage.getItem("wishes") || "[]") - const newWish = { - id: Date.now(), - ...formData, - createdAt: new Date().toISOString(), + // 播放成功音效 + await soundManager.play("success") + + toast({ + title: "你的困擾已成功提交", + description: formData.isPublic + ? "正在為你準備專業的回饋,其他人也能看到你的分享..." + : "正在為你準備專業的回饋,你的分享將保持私密...", + }) + } catch (error) { + console.error("提交困擾失敗:", error) + + // 播放錯誤音效 + await soundManager.play("click") + + toast({ + title: "提交失敗", + description: "請稍後再試或檢查網路連接", + variant: "destructive", + }) + + setIsSubmitting(false) + return } - wishes.push(newWish) - localStorage.setItem("wishes", JSON.stringify(wishes)) - - // 播放成功音效 - await soundManager.play("success") - - toast({ - title: "你的困擾已成功提交", - description: formData.isPublic - ? "正在為你準備專業的回饋,其他人也能看到你的分享..." - : "正在為你準備專業的回饋,你的分享將保持私密...", - }) setFormData({ title: "", @@ -75,8 +117,11 @@ export default function SubmitPage() { expectedSolution: "", expectedEffect: "", isPublic: true, + email: "", }) + setImages([]) setIsSubmitting(false) + setModerationResult(null) // 跳轉到感謝頁面 setTimeout(() => { @@ -354,6 +399,70 @@ export default function SubmitPage() { />
+ {/* 圖片上傳區域 */} +
+ +
+ 上傳與困擾相關的截圖、照片或文件圖片,幫助我們更好地理解問題 +
+ +
+ + {/* Email 聯絡資訊 - 可選 */} +
+ + handleChange("email", e.target.value)} + className="bg-slate-700/50 border-blue-600/50 text-white placeholder:text-blue-300 focus:border-cyan-400 text-sm md:text-base" + /> +
+
+ +
+

完全匿名且可選

+
    +
  • • 你的身份將完全保持匿名,我們不會公開任何個人資訊
  • +
  • • 提供 Email 僅用於我們主動聯繫你,提供個人化的解決方案建議
  • +
  • • 如果不想被聯繫,完全可以留空,不影響困擾的提交和分析
  • +
  • • 我們承諾不會將你的 Email 用於任何行銷或其他用途
  • +
+
+
+
+
+ + {/* 內容審核回饋 */} + {showModerationFeedback && moderationResult && ( + { + const newModeration = moderateWishForm(formData) + setModerationResult(newModeration) + if (newModeration.isAppropriate) { + setShowModerationFeedback(false) + toast({ + title: "內容檢查通過", + description: "現在可以提交你的困擾了!", + }) + } + }} + className="animate-in slide-in-from-top-2 duration-300" + /> + )} + {/* 隱私設定區塊 */}
@@ -391,12 +500,12 @@ export default function SubmitPage() {
{formData.isPublic ? ( - ✅ 你的困擾將會出現在「聆聽心聲」頁面,讓其他人看到並產生共鳴 + ✅ 你的困擾和圖片將會出現在「聆聽心聲」頁面,讓其他人看到並產生共鳴
✅ 同時納入「問題洞察」分析,幫助改善整體工作環境
) : ( - 🔒 你的困擾將保持私密,不會出現在「聆聽心聲」頁面 + 🔒 你的困擾和圖片將保持私密,不會出現在「聆聽心聲」頁面
✅ 仍會納入「問題洞察」分析,幫助開發者和管理者了解問題趨勢
)} @@ -413,6 +522,7 @@ export default function SubmitPage() {
  • • 無論選擇公開或私密,你的個人身份都會保持匿名
  • • 私密分享只用於統計分析,幫助了解整體問題狀況
  • +
  • • 上傳的圖片會與文字內容一起受到相同的隱私保護
  • • 你可以隨時改變想法,但提交後無法修改此設定
  • • 所有數據都會安全保存,僅用於改善工作環境
@@ -437,6 +547,7 @@ export default function SubmitPage() { <> {formData.isPublic ? "公開提交困擾" : "私密提交困擾"} + {images.length > 0 && ({images.length} 張圖片)} )} diff --git a/app/thank-you/page.tsx b/app/thank-you/page.tsx index 0518c52..54153c3 100644 --- a/app/thank-you/page.tsx +++ b/app/thank-you/page.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Sparkles, Heart, Users, ArrowRight, Home, MessageCircle, BarChart3, Eye, EyeOff } from "lucide-react" import HeaderMusicControl from "@/components/header-music-control" +import { WishService } from "@/lib/supabase-service" export default function ThankYouPage() { const [wishes, setWishes] = useState([]) @@ -13,15 +14,48 @@ export default function ThankYouPage() { const [lastWishIsPublic, setLastWishIsPublic] = useState(true) useEffect(() => { - const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]") - setWishes(savedWishes) + const fetchWishes = async () => { + try { + // 獲取所有困擾案例 + const allWishesData = await WishService.getAllWishes() + + // 轉換數據格式 + const convertWish = (wish: any) => ({ + id: wish.id, + title: wish.title, + currentPain: wish.current_pain, + expectedSolution: wish.expected_solution, + expectedEffect: wish.expected_effect || "", + createdAt: wish.created_at, + isPublic: wish.is_public, + email: wish.email, + images: wish.images, + like_count: wish.like_count || 0, // 包含點讚數 + }) + + const allWishes = allWishesData.map(convertWish) + setWishes(allWishes) - // 檢查最後一個提交的願望是否為公開 - if (savedWishes.length > 0) { - const lastWish = savedWishes[savedWishes.length - 1] - setLastWishIsPublic(lastWish.isPublic !== false) + // 檢查最後一個提交的願望是否為公開 + if (allWishes.length > 0) { + const lastWish = allWishes[allWishes.length - 1] + setLastWishIsPublic(lastWish.isPublic !== false) + } + } catch (error) { + console.error("獲取統計數據失敗:", error) + // 如果 Supabase 連接失敗,回退到 localStorage + const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]") + setWishes(savedWishes) + + if (savedWishes.length > 0) { + const lastWish = savedWishes[savedWishes.length - 1] + setLastWishIsPublic(lastWish.isPublic !== false) + } + } } + fetchWishes() + // 延遲顯示內容,創造進入效果 setTimeout(() => setShowContent(true), 300) }, []) @@ -250,7 +284,7 @@ export default function ThankYouPage() { {/* 統計卡片 */}
- +
@@ -261,7 +295,7 @@ export default function ThankYouPage() { - +
@@ -272,7 +306,7 @@ export default function ThankYouPage() { - +
diff --git a/app/wishes/page.tsx b/app/wishes/page.tsx index b37e153..d9c1635 100644 --- a/app/wishes/page.tsx +++ b/app/wishes/page.tsx @@ -9,6 +9,7 @@ import { Sparkles, ArrowLeft, Search, Plus, Filter, X, BarChart3, Eye, Users } f import WishCard from "@/components/wish-card" import HeaderMusicControl from "@/components/header-music-control" import { categories, categorizeWishMultiple, getCategoryStats, type Wish } from "@/lib/categorization" +import { WishService } from "@/lib/supabase-service" export default function WishesPage() { const [wishes, setWishes] = useState([]) @@ -22,15 +23,55 @@ export default function WishesPage() { const [privateCount, setPrivateCount] = useState(0) useEffect(() => { - const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]") - const publicOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic !== false) - const privateOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic === false) + const fetchWishes = async () => { + try { + // 獲取所有困擾(用於統計) + const allWishesData = await WishService.getAllWishes() + + // 獲取公開困擾(用於顯示) + const publicWishesData = await WishService.getPublicWishes() + + // 轉換數據格式以匹配 categorization.ts 的 Wish 接口 + const convertWish = (wish: any) => ({ + id: wish.id, + title: wish.title, + currentPain: wish.current_pain, + expectedSolution: wish.expected_solution, + expectedEffect: wish.expected_effect || "", + createdAt: wish.created_at, + isPublic: wish.is_public, + email: wish.email, + images: wish.images, + like_count: wish.like_count || 0, // 包含點讚數 + }) + + const allWishes = allWishesData.map(convertWish) + const publicWishes = publicWishesData.map(convertWish) + + // 計算私密困擾數量 + const privateCount = allWishes.length - publicWishes.length - setWishes(savedWishes) - setPublicWishes(publicOnly.reverse()) - setTotalWishes(savedWishes.length) - setPrivateCount(privateOnly.length) - setCategoryStats(getCategoryStats(publicOnly)) // 只統計公開的困擾 + setWishes(allWishes) + setPublicWishes(publicWishes) + setTotalWishes(allWishes.length) + setPrivateCount(privateCount) + setCategoryStats(getCategoryStats(publicWishes)) + } catch (error) { + console.error("獲取困擾數據失敗:", error) + // 如果 Supabase 連接失敗,回退到 localStorage + const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]") + const publicOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic !== false) + const privateOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic === false) + + setWishes(savedWishes) + setPublicWishes(publicOnly.reverse()) + setTotalWishes(savedWishes.length) + setPrivateCount(privateOnly.length) + setCategoryStats(getCategoryStats(publicOnly)) + } + } + + fetchWishes() }, []) useEffect(() => { diff --git a/components/content-moderation-feedback.tsx b/components/content-moderation-feedback.tsx new file mode 100644 index 0000000..765d923 --- /dev/null +++ b/components/content-moderation-feedback.tsx @@ -0,0 +1,161 @@ +"use client" + +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { AlertTriangle, Lightbulb, CheckCircle, XCircle, RefreshCw } from "lucide-react" +import type { ModerationResult } from "@/lib/content-moderation" + +interface ContentModerationFeedbackProps { + result: ModerationResult + onRetry: () => void + className?: string +} + +export default function ContentModerationFeedback({ result, onRetry, className = "" }: ContentModerationFeedbackProps) { + if (result.issues.length === 0 && result.suggestions.length === 0) { + return null + } + + const getSeverityColor = (severity: string) => { + switch (severity) { + case "high": + return "border-red-500/50 bg-red-900/20" + case "medium": + return "border-yellow-500/50 bg-yellow-900/20" + case "low": + return "border-blue-500/50 bg-blue-900/20" + default: + return "border-slate-500/50 bg-slate-900/20" + } + } + + const getSeverityIcon = (severity: string) => { + switch (severity) { + case "high": + return + case "medium": + return + case "low": + return + default: + return + } + } + + const getSeverityTitle = (severity: string) => { + switch (severity) { + case "high": + return "內容審核未通過" + case "medium": + return "內容建議優化" + case "low": + return "內容建議" + default: + return "內容檢查" + } + } + + return ( + + + {/* 標題區域 */} +
+ {getSeverityIcon(result.severity)} +
+

{getSeverityTitle(result.severity)}

+
+ + {result.severity === "high" ? "需要修改" : result.severity === "medium" ? "建議優化" : "輕微建議"} + + {!result.isAppropriate && ( + 無法提交 + )} +
+
+ {!result.isAppropriate && ( + + )} +
+ + {/* 問題列表 */} + {result.issues.length > 0 && ( +
+
+ + 發現的問題: +
+
    + {result.issues.map((issue, index) => ( +
  • +
    + {issue} +
  • + ))} +
+
+ )} + + {/* 建議列表 */} + {result.suggestions.length > 0 && ( +
+
+ + 改善建議: +
+
    + {result.suggestions.map((suggestion, index) => ( +
  • +
    + {suggestion} +
  • + ))} +
+
+ )} + + {/* 被阻擋的詞彙 */} + {result.blockedWords.length > 0 && ( +
+
需要修改的詞彙:
+
+ {result.blockedWords.map((word, index) => ( + + {word} + + ))} +
+
+ )} + + {/* 鼓勵訊息 */} + {!result.isAppropriate && ( + + + + 我們理解工作中的挫折和困難。請使用更建設性的語言來描述遇到的問題, + 這樣我們能更好地幫助你找到解決方案。你的每一個真實困擾都很重要! + + + )} +
+
+ ) +} diff --git a/components/image-gallery.tsx b/components/image-gallery.tsx new file mode 100644 index 0000000..8a61467 --- /dev/null +++ b/components/image-gallery.tsx @@ -0,0 +1,214 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { X, ChevronLeft, ChevronRight, Download, ZoomIn, ZoomOut, RotateCw, Maximize2 } from "lucide-react" +import { formatFileSize, type ImageFile } from "@/lib/image-utils" + +interface ImageGalleryProps { + images: ImageFile[] + className?: string +} + +interface ImageModalProps { + images: ImageFile[] + currentIndex: number + onClose: () => void + onNavigate: (index: number) => void +} + +function ImageModal({ images, currentIndex, onClose, onNavigate }: ImageModalProps) { + const [zoom, setZoom] = useState(1) + const [rotation, setRotation] = useState(0) + + const currentImage = images[currentIndex] + + const handlePrevious = () => { + const newIndex = currentIndex > 0 ? currentIndex - 1 : images.length - 1 + onNavigate(newIndex) + setZoom(1) + setRotation(0) + } + + const handleNext = () => { + const newIndex = currentIndex < images.length - 1 ? currentIndex + 1 : 0 + onNavigate(newIndex) + setZoom(1) + setRotation(0) + } + + const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.25, 3)) + const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.25, 0.25)) + const handleRotate = () => setRotation((prev) => (prev + 90) % 360) + + const handleDownload = () => { + // 創建下載連結 + const link = document.createElement("a") + link.href = currentImage.url + link.download = currentImage.name + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + + return ( +
+ {/* 關閉按鈕 */} + + + {/* 工具列 */} +
+ + {currentIndex + 1} / {images.length} + +
+ + {Math.round(zoom * 100)}% + + + +
+
+ + {/* 導航按鈕 */} + {images.length > 1 && ( + <> + + + + )} + + {/* 圖片容器 */} +
+ {currentImage.name} { + // 如果圖片載入失敗,顯示錯誤訊息 + const target = e.target as HTMLImageElement + target.src = "/placeholder.svg?height=400&width=400&text=圖片載入失敗" + }} + /> +
+ + {/* 圖片資訊 */} +
+ + +
+
{currentImage.name}
+
+ {formatFileSize(currentImage.size)} • {currentImage.type} +
+
+
+
+
+
+ ) +} + +export default function ImageGallery({ images, className = "" }: ImageGalleryProps) { + const [modalOpen, setModalOpen] = useState(false) + const [currentImageIndex, setCurrentImageIndex] = useState(0) + + if (images.length === 0) return null + + const openModal = (index: number) => { + setCurrentImageIndex(index) + setModalOpen(true) + } + + const closeModal = () => { + setModalOpen(false) + } + + return ( + <> +
+
+ + 📷 相關圖片 ({images.length}) + +
+ +
+ {images.map((image, index) => ( +
openModal(index)}> +
+ {image.name} { + // 如果圖片載入失敗,顯示預設圖片 + const target = e.target as HTMLImageElement + target.src = "/placeholder.svg?height=200&width=200&text=圖片載入失敗" + }} + /> + + {/* 懸停覆蓋層 */} +
+
+ +
點擊放大
+
+
+
+ + {/* 檔案名稱 */} +
+ {image.name} +
+
+ ))} +
+
+ + {/* 圖片模態框 */} + {modalOpen && ( + + )} + + ) +} diff --git a/components/image-upload.tsx b/components/image-upload.tsx new file mode 100644 index 0000000..303a7cf --- /dev/null +++ b/components/image-upload.tsx @@ -0,0 +1,334 @@ +"use client" + +import type React from "react" + +import { useState, useRef, useCallback } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Upload, X, AlertCircle, Eye, RotateCcw, FileImage } from "lucide-react" +import { + validateImageFile, + createImageFile, + revokeImageUrl, + formatFileSize, + compressImage, + MAX_FILES_PER_UPLOAD, + MAX_TOTAL_FILES, + MAX_FILE_SIZE, + type ImageFile, +} from "@/lib/image-utils" +import { soundManager } from "@/lib/sound-effects" + +interface ImageUploadProps { + images: ImageFile[] + onImagesChange: (images: ImageFile[]) => void + disabled?: boolean + className?: string +} + +export default function ImageUpload({ images, onImagesChange, disabled = false, className = "" }: ImageUploadProps) { + const [dragActive, setDragActive] = useState(false) + const [uploading, setUploading] = useState(false) + const [errors, setErrors] = useState([]) + const fileInputRef = useRef(null) + + const handleFiles = useCallback( + async (files: FileList) => { + if (disabled) return + + setUploading(true) + setErrors([]) + + const newErrors: string[] = [] + const validFiles: File[] = [] + + // 檢查總數量限制 + if (images.length + files.length > MAX_TOTAL_FILES) { + newErrors.push(`最多只能上傳 ${MAX_TOTAL_FILES} 張圖片`) + setErrors(newErrors) + setUploading(false) + return + } + + // 檢查單次上傳數量 + if (files.length > MAX_FILES_PER_UPLOAD) { + newErrors.push(`單次最多只能上傳 ${MAX_FILES_PER_UPLOAD} 張圖片`) + } + + // 驗證每個檔案 + const filesToProcess = Array.from(files).slice(0, MAX_FILES_PER_UPLOAD) + + for (const file of filesToProcess) { + const validation = validateImageFile(file) + if (validation.isValid) { + validFiles.push(file) + } else { + newErrors.push(`${file.name}: ${validation.error}`) + } + } + + if (newErrors.length > 0) { + setErrors(newErrors) + } + + if (validFiles.length > 0) { + try { + // 壓縮並創建圖片物件 + const newImageFiles: ImageFile[] = [] + + for (const file of validFiles) { + let processedFile = file + + // 如果檔案過大,嘗試壓縮 + if (file.size > MAX_FILE_SIZE * 0.8) { + try { + processedFile = await compressImage(file, 1920, 0.8) + } catch (error) { + console.warn("圖片壓縮失敗,使用原檔案:", error) + } + } + + // 轉換為 base64 格式 + const imageFile = await createImageFile(processedFile) + newImageFiles.push(imageFile) + } + + onImagesChange([...images, ...newImageFiles]) + await soundManager.play("success") + } catch (error) { + newErrors.push("圖片處理失敗,請重試") + setErrors(newErrors) + } + } + + setUploading(false) + }, + [images, onImagesChange, disabled], + ) + + const handleDrag = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + }, []) + + const handleDragIn = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setDragActive(true) + } + }, []) + + const handleDragOut = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragActive(false) + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragActive(false) + + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFiles(e.dataTransfer.files) + } + }, + [handleFiles], + ) + + const handleFileInput = useCallback( + (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + handleFiles(e.target.files) + } + // 清空 input 值,允許重複選擇相同檔案 + e.target.value = "" + }, + [handleFiles], + ) + + const removeImage = useCallback( + async (imageId: string) => { + const imageToRemove = images.find((img) => img.id === imageId) + if (imageToRemove) { + revokeImageUrl(imageToRemove) + onImagesChange(images.filter((img) => img.id !== imageId)) + await soundManager.play("click") + } + }, + [images, onImagesChange], + ) + + const clearAllImages = useCallback(async () => { + images.forEach(revokeImageUrl) + onImagesChange([]) + setErrors([]) + await soundManager.play("click") + }, [images, onImagesChange]) + + return ( +
+ {/* 上傳區域 */} + !disabled && fileInputRef.current?.click()} + > + +
+
+ {uploading ? ( +
+ ) : ( + + )} +
+ +
+

{dragActive ? "放開以上傳圖片" : "上傳相關圖片"}

+

{uploading ? "正在處理圖片..." : "拖拽圖片到此處或點擊選擇檔案"}

+
+ + 支援 JPG、PNG、WebP、GIF + + + 單檔最大 5MB + + + 最多 {MAX_TOTAL_FILES} 張 + +
+
+
+ + + + + + {/* 錯誤訊息 */} + {errors.length > 0 && ( + + + +
+ {errors.map((error, index) => ( +
+ • {error} +
+ ))} +
+
+
+ )} + + {/* 已上傳的圖片 */} + {images.length > 0 && ( + + +
+
+ +

+ 已上傳圖片 ({images.length}/{MAX_TOTAL_FILES}) +

+
+ {images.length > 1 && ( + + )} +
+ +
+ {images.map((image) => ( +
+
+ {image.name} { + // 如果圖片載入失敗,顯示預設圖片 + const target = e.target as HTMLImageElement + target.src = "/placeholder.svg?height=200&width=200&text=圖片載入失敗" + }} + /> + + {/* 懸停覆蓋層 */} +
+ + +
+
+ + {/* 檔案資訊 */} +
+
+ {image.name} +
+
{formatFileSize(image.size)}
+
+
+ ))} +
+
+
+ )} +
+ ) +} diff --git a/components/migration-dialog.tsx b/components/migration-dialog.tsx new file mode 100644 index 0000000..a812d39 --- /dev/null +++ b/components/migration-dialog.tsx @@ -0,0 +1,314 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Progress } from "@/components/ui/progress" +import { Database, Upload, CheckCircle, XCircle, AlertTriangle, Loader2, Trash2, RefreshCw } from "lucide-react" +import { MigrationService, testSupabaseConnection } from "@/lib/supabase-service" + +interface MigrationDialogProps { + onComplete?: () => void + onSkip?: () => void +} + +export default function MigrationDialog({ onComplete, onSkip }: MigrationDialogProps) { + const [step, setStep] = useState<"check" | "migrate" | "complete" | "error">("check") + const [localDataCount, setLocalDataCount] = useState(0) + const [migrationResult, setMigrationResult] = useState<{ + success: number + failed: number + errors: string[] + } | null>(null) + const [isConnected, setIsConnected] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [progress, setProgress] = useState(0) + + useEffect(() => { + checkLocalData() + checkConnection() + }, []) + + const checkLocalData = () => { + try { + const wishes = JSON.parse(localStorage.getItem("wishes") || "[]") + setLocalDataCount(wishes.length) + } catch (error) { + console.error("Error checking local data:", error) + setLocalDataCount(0) + } + } + + const checkConnection = async () => { + setIsLoading(true) + try { + const connected = await testSupabaseConnection() + setIsConnected(connected) + } catch (error) { + console.error("Connection check failed:", error) + setIsConnected(false) + } finally { + setIsLoading(false) + } + } + + const startMigration = async () => { + if (!isConnected) { + alert("請先確保 Supabase 連接正常") + return + } + + setStep("migrate") + setIsLoading(true) + setProgress(0) + + try { + // 模擬進度更新 + const progressInterval = setInterval(() => { + setProgress((prev) => Math.min(prev + 10, 90)) + }, 200) + + const result = await MigrationService.migrateWishesFromLocalStorage() + + clearInterval(progressInterval) + setProgress(100) + + setMigrationResult(result) + + if (result.success > 0) { + setStep("complete") + } else { + setStep("error") + } + } catch (error) { + console.error("Migration failed:", error) + setMigrationResult({ + success: 0, + failed: localDataCount, + errors: [`遷移過程失敗: ${error}`], + }) + setStep("error") + } finally { + setIsLoading(false) + } + } + + const clearLocalData = () => { + if (confirm("確定要清除本地數據嗎?此操作無法復原。")) { + MigrationService.clearLocalStorageData() + setLocalDataCount(0) + onComplete?.() + } + } + + const skipMigration = () => { + if (confirm("跳過遷移將繼續使用本地存儲。確定要跳過嗎?")) { + onSkip?.() + } + } + + if (localDataCount === 0) { + return ( + + + + + 準備就緒 + + 沒有發現本地數據,可以直接開始使用 Supabase + + + + + + ) + } + + return ( + + + + + 數據遷移到 Supabase + + + 發現 {localDataCount} 個本地困擾案例,建議遷移到雲端數據庫 + + + + {/* 連接狀態 */} +
+
+
+ Supabase 連接狀態 +
+
+ + {isConnected ? "已連接" : "未連接"} + + +
+
+ + {step === "check" && ( +
+ + + +
+

+ 遷移優勢: +

+
    +
  • • 數據永久保存,不會因清除瀏覽器而丟失
  • +
  • • 支援多設備同步訪問
  • +
  • • 更好的性能和穩定性
  • +
  • • 支援更多用戶同時使用
  • +
+
+
+
+ +
+ + +
+
+ )} + + {step === "migrate" && ( +
+
+ +

正在遷移數據...

+
+ +

請稍候,正在將 {localDataCount} 個案例遷移到雲端

+
+ )} + + {step === "complete" && migrationResult && ( +
+
+ +

遷移完成!

+
+ +
+
+
{migrationResult.success}
+
成功遷移
+
+
+
{migrationResult.failed}
+
遷移失敗
+
+
+ + {migrationResult.errors.length > 0 && ( + + + +
+ 查看錯誤詳情 +
+ {migrationResult.errors.map((error, index) => ( +
• {error}
+ ))} +
+
+
+
+ )} + +
+ + +
+
+ )} + + {step === "error" && migrationResult && ( +
+
+ +

遷移失敗

+
+ + + + +
+

遷移過程中遇到問題:

+
+ {migrationResult.errors.map((error, index) => ( +
• {error}
+ ))} +
+
+
+
+ +
+ + +
+
+ )} +
+
+ ) +} diff --git a/components/supabase-image-upload.tsx b/components/supabase-image-upload.tsx new file mode 100644 index 0000000..fb149f9 --- /dev/null +++ b/components/supabase-image-upload.tsx @@ -0,0 +1,394 @@ +"use client" + +import type React from "react" + +import { useState, useRef, useCallback } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Progress } from "@/components/ui/progress" +import { Upload, X, AlertCircle, Eye, RotateCcw, FileImage, Cloud, Loader2 } from "lucide-react" +import { + SupabaseImageService, + ImageCompressionService, + type SupabaseImageFile, + type BatchUploadResult, +} from "@/lib/supabase-image-utils" +import { soundManager } from "@/lib/sound-effects" + +interface SupabaseImageUploadProps { + images: SupabaseImageFile[] + onImagesChange: (images: SupabaseImageFile[]) => void + disabled?: boolean + className?: string + maxFiles?: number +} + +export default function SupabaseImageUpload({ + images, + onImagesChange, + disabled = false, + className = "", + maxFiles = 10, +}: SupabaseImageUploadProps) { + const [dragActive, setDragActive] = useState(false) + const [uploading, setUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + const [errors, setErrors] = useState([]) + const [uploadStats, setUploadStats] = useState<{ total: 0; completed: 0; failed: 0 }>({ + total: 0, + completed: 0, + failed: 0, + }) + const fileInputRef = useRef(null) + + const handleFiles = useCallback( + async (files: FileList) => { + if (disabled) return + + setUploading(true) + setErrors([]) + setUploadProgress(0) + + const fileArray = Array.from(files) + const remainingSlots = maxFiles - images.length + + if (fileArray.length > remainingSlots) { + setErrors([`最多只能再上傳 ${remainingSlots} 張圖片`]) + setUploading(false) + return + } + + try { + // 初始化統計 + setUploadStats({ total: fileArray.length, completed: 0, failed: 0 }) + + // 先壓縮圖片 + setUploadProgress(10) + const compressedFiles = await ImageCompressionService.compressImages(fileArray) + setUploadProgress(20) + + // 批量上傳到 Supabase Storage + const uploadResult: BatchUploadResult = await SupabaseImageService.uploadImages(compressedFiles) + + // 更新統計 + setUploadStats({ + total: uploadResult.total, + completed: uploadResult.successful.length, + failed: uploadResult.failed.length, + }) + + // 處理上傳結果 + if (uploadResult.successful.length > 0) { + onImagesChange([...images, ...uploadResult.successful]) + await soundManager.play("success") + } + + // 處理錯誤 + if (uploadResult.failed.length > 0) { + const errorMessages = uploadResult.failed.map((failure) => `${failure.file.name}: ${failure.error}`) + setErrors(errorMessages) + } + + setUploadProgress(100) + } catch (error) { + console.error("Upload process error:", error) + setErrors([`上傳過程中發生錯誤: ${error}`]) + } finally { + setTimeout(() => { + setUploading(false) + setUploadProgress(0) + setUploadStats({ total: 0, completed: 0, failed: 0 }) + }, 2000) + } + }, + [images, onImagesChange, disabled, maxFiles], + ) + + const handleDrag = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + }, []) + + const handleDragIn = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setDragActive(true) + } + }, []) + + const handleDragOut = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragActive(false) + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragActive(false) + + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFiles(e.dataTransfer.files) + } + }, + [handleFiles], + ) + + const handleFileInput = useCallback( + (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + handleFiles(e.target.files) + } + e.target.value = "" + }, + [handleFiles], + ) + + const removeImage = useCallback( + async (imageId: string) => { + const imageToRemove = images.find((img) => img.id === imageId) + if (imageToRemove) { + // 從 Supabase Storage 刪除圖片 + const deleteResult = await SupabaseImageService.deleteImage(imageToRemove.storage_path) + if (!deleteResult.success) { + console.warn("Failed to delete image from storage:", deleteResult.error) + // 即使刪除失敗,也從列表中移除(避免阻塞用戶操作) + } + + onImagesChange(images.filter((img) => img.id !== imageId)) + await soundManager.play("click") + } + }, + [images, onImagesChange], + ) + + const clearAllImages = useCallback(async () => { + if (images.length === 0) return + + const storagePaths = images.map((img) => img.storage_path) + const deleteResult = await SupabaseImageService.deleteImages(storagePaths) + + if (!deleteResult.success) { + console.warn("Failed to delete some images from storage:", deleteResult.error) + } + + onImagesChange([]) + setErrors([]) + await soundManager.play("click") + }, [images, onImagesChange]) + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] + } + + return ( +
+ {/* 上傳區域 */} + !disabled && !uploading && fileInputRef.current?.click()} + > + +
+
+ {uploading ? ( + + ) : ( +
+ + +
+ )} +
+ +
+

+ {uploading ? "正在上傳到雲端..." : dragActive ? "放開以上傳圖片" : "上傳相關圖片到雲端"} +

+

+ {uploading ? "圖片將安全存儲在 Supabase 雲端" : "拖拽圖片到此處或點擊選擇檔案"} +

+
+ + + 雲端存儲 + + + 支援 JPG、PNG、WebP、GIF + + + 單檔最大 5MB + + + 最多 {maxFiles} 張 + +
+
+
+ + {/* 上傳進度 */} + {uploading && ( +
+ +
+ + {uploadStats.completed}/{uploadStats.total} 完成 + + {uploadProgress}% +
+ {uploadStats.failed > 0 && ( +
{uploadStats.failed} 個檔案上傳失敗
+ )} +
+ )} +
+ + +
+ + {/* 錯誤訊息 */} + {errors.length > 0 && ( + + + +
+ {errors.map((error, index) => ( +
+ • {error} +
+ ))} +
+
+
+ )} + + {/* 已上傳的圖片 */} + {images.length > 0 && ( + + +
+
+ +

+ 雲端圖片 ({images.length}/{maxFiles}) +

+ + + Supabase + +
+ {images.length > 1 && ( + + )} +
+ +
+ {images.map((image) => ( +
+
+ {image.name} { + const target = e.target as HTMLImageElement + target.src = "/placeholder.svg?height=200&width=200&text=圖片載入失敗" + }} + /> + + {/* 懸停覆蓋層 */} +
+ + +
+ + {/* 雲端標識 */} +
+ + + +
+
+ + {/* 檔案資訊 */} +
+
+ {image.name} +
+
+ {formatFileSize(image.size)} + 雲端 +
+
+
+ ))} +
+
+
+ )} +
+ ) +} diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx index c31c2b3..42e4ef1 100644 --- a/components/ui/slider.tsx +++ b/components/ui/slider.tsx @@ -17,10 +17,10 @@ const Slider = React.forwardRef< )} {...props} > - - + + - + )) Slider.displayName = SliderPrimitive.Root.displayName diff --git a/components/wish-card.tsx b/components/wish-card.tsx index 793c02f..1c74e13 100644 --- a/components/wish-card.tsx +++ b/components/wish-card.tsx @@ -20,9 +20,12 @@ import { categorizeWishMultiple, type Wish } from "@/lib/categorization" import { generateSolutionRecommendations, type SolutionCategory } from "@/lib/solution-recommendations" import { useState, useEffect } from "react" import { soundManager } from "@/lib/sound-effects" +import ImageGallery from "@/components/image-gallery" +import { restoreImageFile, type ImageFile } from "@/lib/image-utils" +import { LikeService } from "@/lib/supabase-service" interface WishCardProps { - wish: Wish + wish: Wish & { images?: any[]; like_count?: number } // 添加圖片支援和點讚數 } export default function WishCard({ wish }: WishCardProps) { @@ -34,12 +37,29 @@ export default function WishCard({ wish }: WishCardProps) { // 載入點讚數據 useEffect(() => { - const likes = JSON.parse(localStorage.getItem("wishLikes") || "{}") - const likedWishes = JSON.parse(localStorage.getItem("userLikedWishes") || "[]") + const loadLikeData = async () => { + try { + // 從 Supabase 獲取用戶已點讚的困擾列表 + const userLikedWishes = await LikeService.getUserLikedWishes() + + // 設置點讚狀態 + setHasLiked(userLikedWishes.includes(wish.id)) + + // 點讚數從 wish 的 like_count 字段獲取,如果沒有則默認為 0 + setLikeCount(wish.like_count || 0) + } catch (error) { + console.error("載入點讚數據失敗:", error) + // 如果 Supabase 連接失敗,回退到 localStorage + const likes = JSON.parse(localStorage.getItem("wishLikes") || "{}") + const likedWishes = JSON.parse(localStorage.getItem("userLikedWishes") || "[]") - setLikeCount(likes[wish.id] || 0) - setHasLiked(likedWishes.includes(wish.id)) - }, [wish.id]) + setLikeCount(likes[wish.id] || 0) + setHasLiked(likedWishes.includes(wish.id)) + } + } + + loadLikeData() + }, [wish.id, wish.like_count]) const handleLike = async () => { if (hasLiked || isLiking) return @@ -49,24 +69,46 @@ export default function WishCard({ wish }: WishCardProps) { // 播放點讚音效 await soundManager.play("click") - // 更新點讚數據 - const likes = JSON.parse(localStorage.getItem("wishLikes") || "{}") - const likedWishes = JSON.parse(localStorage.getItem("userLikedWishes") || "[]") + try { + // 使用 Supabase 點讚服務 + const success = await LikeService.likeWish(wish.id) + + if (success) { + // 更新本地狀態 + setLikeCount(prev => prev + 1) + setHasLiked(true) + + // 播放成功音效 + setTimeout(async () => { + await soundManager.play("success") + }, 300) + } else { + // 已經點讚過 + console.log("已經點讚過此困擾") + } + } catch (error) { + console.error("點讚失敗:", error) + + // 如果 Supabase 失敗,回退到 localStorage + const likes = JSON.parse(localStorage.getItem("wishLikes") || "{}") + const likedWishes = JSON.parse(localStorage.getItem("userLikedWishes") || "[]") - likes[wish.id] = (likes[wish.id] || 0) + 1 - likedWishes.push(wish.id) + likes[wish.id] = (likes[wish.id] || 0) + 1 + likedWishes.push(wish.id) - localStorage.setItem("wishLikes", JSON.stringify(likes)) - localStorage.setItem("userLikedWishes", JSON.stringify(likedWishes)) + localStorage.setItem("wishLikes", JSON.stringify(likes)) + localStorage.setItem("userLikedWishes", JSON.stringify(likedWishes)) - setLikeCount(likes[wish.id]) - setHasLiked(true) - - // 播放成功音效 - setTimeout(async () => { - await soundManager.play("success") + setLikeCount(likes[wish.id]) + setHasLiked(true) + + // 播放成功音效 + setTimeout(async () => { + await soundManager.play("success") + }, 300) + } finally { setIsLiking(false) - }, 300) + } } const formatDate = (dateString: string) => { @@ -84,6 +126,9 @@ export default function WishCard({ wish }: WishCardProps) { // 生成解決方案建議 const solutionRecommendation = generateSolutionRecommendations(wish) + // 轉換圖片數據格式 - 使用 restoreImageFile 恢復圖片 + const images: ImageFile[] = (wish.images || []).map((img) => restoreImageFile(img)) + const getDifficultyColor = (difficulty: string) => { switch (difficulty) { case "easy": @@ -220,6 +265,16 @@ export default function WishCard({ wish }: WishCardProps) {
)} + {/* 圖片展示區域 */} + {images.length > 0 && ( +
+
+
+ +
+
+ )} + {/* 共鳴支持區塊 - 新增 */}
diff --git a/lib/background-music-supabase.ts b/lib/background-music-supabase.ts new file mode 100644 index 0000000..f1b9352 --- /dev/null +++ b/lib/background-music-supabase.ts @@ -0,0 +1,249 @@ +import { UserSettingsService } from "./supabase-service" + +// 背景音樂管理系統 - Supabase 版本 +class BackgroundMusicManagerSupabase { + private audio: HTMLAudioElement | null = null + private isPlaying = false + private enabled = false + private volume = 0.3 + private fadeInterval: NodeJS.Timeout | null = null + private initialized = false + + constructor() { + this.initAudio() + } + + // 初始化並載入用戶設定 + async init() { + if (this.initialized) return + + try { + const settings = await UserSettingsService.getUserSettings() + if (settings) { + this.volume = settings.background_music_volume + this.enabled = settings.background_music_enabled + this.isPlaying = false // 不自動播放 + } + this.initialized = true + } catch (error) { + console.error("Failed to load user settings:", error) + // 使用默認設定 + this.volume = 0.3 + this.enabled = false + this.isPlaying = false + this.initialized = true + } + } + + private initAudio() { + try { + this.audio = new Audio("https://hebbkx1anhila5yf.public.blob.vercel-storage.com/just-relax-11157-iAgp15dV2YGybAezUJFtKmKZPbteXd.mp3") + this.audio.loop = true + this.audio.volume = this.volume + this.audio.preload = "metadata" + this.audio.autoplay = false + this.audio.muted = false + + this.audio.addEventListener("canplaythrough", () => { + // 音樂載入完成 + }) + + this.audio.addEventListener("error", (e) => { + this.reinitAudio() + }) + + this.audio.addEventListener("ended", () => { + if (this.enabled && this.isPlaying) { + this.audio?.play().catch(() => { + this.reinitAudio() + }) + } + }) + } catch (error) { + console.error("Audio initialization failed:", error) + } + } + + private reinitAudio() { + try { + if (this.audio) { + this.audio.pause() + this.audio.src = "" + this.audio = null + } + setTimeout(() => { + this.initAudio() + }, 100) + } catch (error) { + console.error("Audio reinitialization failed:", error) + } + } + + private fadeIn(duration = 2000) { + if (!this.audio) return + + this.audio.volume = 0 + const targetVolume = this.volume + const steps = 50 + const stepTime = duration / steps + const volumeStep = targetVolume / steps + + let currentStep = 0 + this.fadeInterval = setInterval(() => { + if (currentStep >= steps || !this.audio) { + if (this.fadeInterval) { + clearInterval(this.fadeInterval) + this.fadeInterval = null + } + if (this.audio) { + this.audio.volume = targetVolume + } + return + } + + this.audio.volume = Math.min(volumeStep * currentStep, targetVolume) + currentStep++ + }, stepTime) + } + + private fadeOut(duration = 1000) { + if (!this.audio) return + + const startVolume = this.audio.volume + const steps = 50 + const stepTime = duration / steps + const volumeStep = startVolume / steps + + let currentStep = 0 + this.fadeInterval = setInterval(() => { + if (currentStep >= steps || !this.audio) { + if (this.fadeInterval) { + clearInterval(this.fadeInterval) + this.fadeInterval = null + } + if (this.audio) { + this.audio.pause() + this.audio.currentTime = 0 + this.audio.volume = this.volume + } + return + } + + this.audio.volume = Math.max(startVolume - volumeStep * currentStep, 0) + currentStep++ + }, stepTime) + } + + async start() { + if (!this.initialized) await this.init() + + if (this.fadeInterval) { + clearInterval(this.fadeInterval) + this.fadeInterval = null + } + + if (!this.audio) { + this.initAudio() + await new Promise((resolve) => setTimeout(resolve, 100)) + } + + if (!this.audio) return + + try { + this.enabled = true + this.isPlaying = true + + // 保存設定到 Supabase + await this.saveSettings() + + this.audio.currentTime = 0 + this.audio.volume = 0 + + await this.audio.play() + this.fadeIn(2000) + } catch (error) { + console.error("Failed to start music:", error) + this.reinitAudio() + this.isPlaying = false + this.enabled = false + await this.saveSettings() + } + } + + async stop() { + if (!this.initialized) await this.init() + + if (!this.audio) return + + this.enabled = false + this.isPlaying = false + + // 保存設定到 Supabase + await this.saveSettings() + + if (this.fadeInterval) { + clearInterval(this.fadeInterval) + this.fadeInterval = null + } + + this.fadeOut(1000) + } + + async setVolume(volume: number) { + if (!this.initialized) await this.init() + + this.volume = Math.max(0, Math.min(1, volume)) + if (this.audio && this.isPlaying) { + this.audio.volume = this.volume + } + + // 保存設定到 Supabase + await this.saveSettings() + } + + // 保存設定到 Supabase + private async saveSettings() { + try { + await UserSettingsService.updateUserSettings({ + backgroundMusicEnabled: this.enabled, + backgroundMusicVolume: this.volume, + backgroundMusicPlaying: this.isPlaying, + }) + } catch (error) { + console.error("Failed to save music settings:", error) + } + } + + getVolume() { + return this.volume + } + + isEnabled() { + return this.enabled + } + + getIsPlaying() { + return this.isPlaying && this.audio && !this.audio.paused + } + + getState() { + return { + isPlaying: this.getIsPlaying(), + enabled: this.enabled, + volume: this.volume, + } + } + + getMusicInfo() { + if (!this.audio) return null + + return { + duration: this.audio.duration || 0, + currentTime: this.audio.currentTime || 0, + loaded: this.audio.readyState >= 3, + } + } +} + +// 全局背景音樂管理器 - Supabase 版本 +export const backgroundMusicManagerSupabase = new BackgroundMusicManagerSupabase() diff --git a/lib/categorization.ts b/lib/categorization.ts index 2938b45..65e3f63 100644 --- a/lib/categorization.ts +++ b/lib/categorization.ts @@ -49,9 +49,9 @@ export const categories = [ "報告", "圖表", ], - color: "#EC4899", - bgColor: "from-pink-500/20 to-rose-600/20", - borderColor: "border-pink-400/30", + color: "#DB2777", + bgColor: "from-pink-600/20 to-rose-700/20", + borderColor: "border-pink-500/30", textColor: "text-pink-200", icon: "📊", }, @@ -322,10 +322,10 @@ export function categorizeWishMultiple(wish: Wish): Category[] { name: "其他問題", description: "未能歸類的特殊工作困擾", keywords: [], - color: "#6B7280", - bgColor: "from-gray-500/20 to-slate-600/20", - borderColor: "border-gray-400/30", - textColor: "text-gray-200", + color: "#94A3B8", + bgColor: "from-slate-400/20 to-slate-500/20", + borderColor: "border-slate-400/40", + textColor: "text-slate-200", icon: "❓", }, ] diff --git a/lib/content-moderation.ts b/lib/content-moderation.ts new file mode 100644 index 0000000..8fb3c3e --- /dev/null +++ b/lib/content-moderation.ts @@ -0,0 +1,249 @@ +// 內容審核 AI 系統 +export interface ModerationResult { + isAppropriate: boolean + issues: string[] + suggestions: string[] + severity: "low" | "medium" | "high" + blockedWords: string[] +} + +// 不雅詞彙和辱罵詞彙庫 +const inappropriateWords = [ + // 髒話和不雅詞彙 + "幹", + "靠", + "操", + "媽的", + "他媽的", + "去死", + "死", + "滾", + "白痴", + "智障", + "腦殘", + "垃圾", + "廢物", + "混蛋", + "王八蛋", + "狗屎", + "屎", + "婊子", + "賤", + "爛", + "鳥", + "屌", + "雞掰", + "機掰", + "北七", + + // 公司辱罵相關 + "爛公司", + "垃圾公司", + "黑心公司", + "慣老闆", + "奴隸主", + "血汗工廠", + "剝削", + "壓榨", + "去你的", + "見鬼", + "該死", + "要死", + "找死", + "活該", + "報應", + "天殺的", + + // 威脅性詞彙 + "殺", + "打死", + "弄死", + "搞死", + "整死", + "報復", + "復仇", + "毀掉", + "搞垮", + + // 歧視性詞彙 + "歧視", + "種族", + "性別歧視", + "老不死", + "死老頭", + "死老太婆", + "殘廢", + "瘸子", + + // 英文不雅詞彙 + "fuck", + "shit", + "damn", + "bitch", + "asshole", + "bastard", + "crap", + "hell", + "wtf", + "stfu", + "bullshit", + "motherfucker", + "dickhead", + "piss", +] + +// 負面但可接受的詞彙(會給予建議但不阻擋) +const negativeButAcceptableWords = [ + "討厭", + "煩", + "累", + "辛苦", + "困難", + "挫折", + "失望", + "無奈", + "痛苦", + "壓力", + "不滿", + "抱怨", + "不爽", + "生氣", + "憤怒", + "沮喪", + "絕望", + "疲憊", + "厭倦", +] + +// 建設性詞彙建議 +const constructiveSuggestions = [ + "建議使用更具體的描述來說明遇到的困難", + "可以嘗試描述期望的改善方向", + "分享具體的情況會更有助於找到解決方案", + "描述問題的影響程度會幫助我們更好地理解", + "可以說明這個問題對工作效率的具體影響", +] + +export function moderateContent(content: string): ModerationResult { + const fullText = content.toLowerCase() + const issues: string[] = [] + const suggestions: string[] = [] + const blockedWords: string[] = [] + let severity: "low" | "medium" | "high" = "low" + + // 檢查不雅詞彙 + inappropriateWords.forEach((word) => { + if (fullText.includes(word.toLowerCase())) { + blockedWords.push(word) + issues.push(`包含不適當詞彙: "${word}"`) + } + }) + + // 檢查負面但可接受的詞彙 + const negativeWordCount = negativeButAcceptableWords.filter((word) => fullText.includes(word.toLowerCase())).length + + // 判斷嚴重程度 + if (blockedWords.length > 0) { + severity = "high" + issues.push("內容包含不雅或辱罵詞彙,無法提交") + suggestions.push("請使用更專業和建設性的語言描述遇到的困難") + suggestions.push("我們理解工作中的挫折,但希望能以正面的方式表達") + } else if (negativeWordCount > 3) { + severity = "medium" + issues.push("內容情緒較為負面") + suggestions.push("建議加入一些具體的改善建議或期望") + suggestions.push("描述具體情況會比情緒性詞彙更有幫助") + } else if (negativeWordCount > 1) { + severity = "low" + suggestions.push("可以嘗試更具體地描述遇到的挑戰") + } + + // 內容長度檢查 + if (content.trim().length < 10) { + issues.push("內容過於簡短,請提供更詳細的描述") + severity = severity === "low" ? "medium" : severity + } + + // 重複字符檢查(可能是情緒性表達) + const repeatedChars = content.match(/(.)\1{4,}/g) + if (repeatedChars) { + issues.push("請避免使用過多重複字符") + suggestions.push("建議使用清楚的文字描述來表達感受") + } + + // 全大寫檢查(可能是憤怒表達) + const upperCaseRatio = (content.match(/[A-Z]/g) || []).length / content.length + if (upperCaseRatio > 0.5 && content.length > 20) { + issues.push("請避免使用過多大寫字母") + suggestions.push("正常的大小寫會讓內容更容易閱讀") + } + + // 如果沒有具體建議,添加通用建議 + if (suggestions.length === 0 && severity !== "high") { + suggestions.push(...constructiveSuggestions.slice(0, 2)) + } + + return { + isAppropriate: blockedWords.length === 0, + issues, + suggestions, + severity, + blockedWords, + } +} + +// 檢查整個表單內容 +export function moderateWishForm(formData: { + title: string + currentPain: string + expectedSolution: string + expectedEffect: string +}): ModerationResult { + const allContent = `${formData.title} ${formData.currentPain} ${formData.expectedSolution} ${formData.expectedEffect}` + + const result = moderateContent(allContent) + + // 針對不同欄位給出具體建議 + const fieldSpecificSuggestions: string[] = [] + + if (formData.title.length < 5) { + fieldSpecificSuggestions.push('標題建議更具體一些,例如:"資料整理效率低下" 而非 "很煩"') + } + + if (formData.currentPain.length < 20) { + fieldSpecificSuggestions.push("困擾描述可以更詳細,包括具體情況和影響") + } + + if (formData.expectedSolution.length < 15) { + fieldSpecificSuggestions.push("期望解決方式可以更具體,這有助於我們提供更好的建議") + } + + return { + ...result, + suggestions: [...result.suggestions, ...fieldSpecificSuggestions], + } +} + +// 提供正面的表達建議 +export function getSuggestedPhrases(originalText: string): string[] { + const suggestions: string[] = [] + + // 根據內容提供建議 + if (originalText.includes("很煩") || originalText.includes("討厭")) { + suggestions.push('可以說:"這個流程讓我感到困擾,希望能夠簡化"') + } + + if (originalText.includes("爛") || originalText.includes("垃圾")) { + suggestions.push('可以說:"這個系統存在一些問題,影響了工作效率"') + } + + if (originalText.includes("老闆") && (originalText.includes("討厌") || originalText.includes("爛"))) { + suggestions.push('可以說:"希望能與主管有更好的溝通和協作"') + } + + if (originalText.includes("同事")) { + suggestions.push('可以說:"團隊協作方面遇到一些挑戰"') + } + + return suggestions +} diff --git a/lib/email-validation.ts b/lib/email-validation.ts new file mode 100644 index 0000000..9fc7a74 --- /dev/null +++ b/lib/email-validation.ts @@ -0,0 +1,60 @@ +// Email 驗證工具 +export function isValidEmail(email: string): boolean { + if (!email) return true // 空值是允許的(可選欄位) + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) +} + +export function sanitizeEmail(email: string): string { + return email.trim().toLowerCase() +} + +export function getEmailDomain(email: string): string { + const parts = email.split("@") + return parts.length > 1 ? parts[1] : "" +} + +// 檢查是否為常見的臨時郵箱服務 +const temporaryEmailDomains = [ + "10minutemail.com", + "guerrillamail.com", + "mailinator.com", + "tempmail.org", + "throwaway.email", +] + +export function isTemporaryEmail(email: string): boolean { + if (!email) return false + + const domain = getEmailDomain(email) + return temporaryEmailDomains.includes(domain) +} + +export function validateEmailForSubmission(email: string): { + isValid: boolean + message?: string + suggestion?: string +} { + if (!email) { + return { isValid: true } // 空值允許 + } + + if (!isValidEmail(email)) { + return { + isValid: false, + message: "請輸入有效的 Email 格式", + suggestion: "例如:your.name@company.com", + } + } + + if (isTemporaryEmail(email)) { + return { + isValid: false, + message: "請避免使用臨時郵箱服務", + suggestion: "建議使用常用的 Email 地址,以便我們能夠聯繫到你", + } + } + + return { isValid: true } +} diff --git a/lib/image-utils.ts b/lib/image-utils.ts new file mode 100644 index 0000000..3d5462b --- /dev/null +++ b/lib/image-utils.ts @@ -0,0 +1,190 @@ +// 圖片處理工具 +export interface ImageFile { + id: string + file?: File // 可選,因為從 localStorage 恢復時不會有原始 File + url: string // 改為 base64 URL + name: string + size: number + type: string + base64?: string // 新增 base64 字段 +} + +export interface ImageValidationResult { + isValid: boolean + error?: string + suggestion?: string +} + +// 允許的圖片格式 +export const ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"] + +// 允許的檔案副檔名 +export const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif"] + +// 檔案大小限制 (5MB) +export const MAX_FILE_SIZE = 5 * 1024 * 1024 + +// 單次上傳數量限制 +export const MAX_FILES_PER_UPLOAD = 10 + +// 總檔案數量限制 +export const MAX_TOTAL_FILES = 20 + +export function validateImageFile(file: File): ImageValidationResult { + // 檢查檔案類型 + if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { + return { + isValid: false, + error: `不支援的檔案格式: ${file.type}`, + suggestion: `請使用 JPG、PNG、WebP 或 GIF 格式`, + } + } + + // 檢查檔案大小 + if (file.size > MAX_FILE_SIZE) { + const sizeMB = (file.size / (1024 * 1024)).toFixed(1) + return { + isValid: false, + error: `檔案過大: ${sizeMB}MB`, + suggestion: `請壓縮圖片至 5MB 以下`, + } + } + + // 檢查檔案名稱 + if (file.name.length > 100) { + return { + isValid: false, + error: "檔案名稱過長", + suggestion: "請使用較短的檔案名稱", + } + } + + return { isValid: true } +} + +export function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes" + + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i] +} + +// 將 File 轉換為 base64 +export function fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + if (typeof reader.result === "string") { + resolve(reader.result) + } else { + reject(new Error("Failed to convert file to base64")) + } + } + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(file) + }) +} + +// 創建圖片文件對象(使用 base64) +export async function createImageFile(file: File): Promise { + const base64 = await fileToBase64(file) + + return { + id: `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + file, + url: base64, // 直接使用 base64 作為 URL + name: file.name, + size: file.size, + type: file.type, + base64, + } +} + +// 從儲存的數據恢復圖片對象 +export function restoreImageFile(data: any): ImageFile { + return { + id: data.id, + url: data.base64 || data.url, // 優先使用 base64,向後兼容 + name: data.name, + size: data.size, + type: data.type, + base64: data.base64, + } +} + +// 不再需要 revokeImageUrl,因為使用 base64 +export function revokeImageUrl(imageFile: ImageFile): void { + // base64 不需要手動釋放 + return +} + +// 壓縮圖片並轉為 base64 +export function compressImage(file: File, maxWidth = 1920, quality = 0.8): Promise { + return new Promise((resolve) => { + const canvas = document.createElement("canvas") + const ctx = canvas.getContext("2d") + const img = new Image() + + img.onload = () => { + // 計算新尺寸 + let { width, height } = img + + if (width > maxWidth) { + height = (height * maxWidth) / width + width = maxWidth + } + + canvas.width = width + canvas.height = height + + // 繪製壓縮後的圖片 + ctx?.drawImage(img, 0, 0, width, height) + + canvas.toBlob( + (blob) => { + if (blob) { + const compressedFile = new File([blob], file.name, { + type: file.type, + lastModified: Date.now(), + }) + resolve(compressedFile) + } else { + resolve(file) // 如果壓縮失敗,返回原檔案 + } + }, + file.type, + quality, + ) + } + + img.src = URL.createObjectURL(file) + }) +} + +// 生成縮圖 +export function generateThumbnail(file: File, size = 200): Promise { + return new Promise((resolve) => { + const canvas = document.createElement("canvas") + const ctx = canvas.getContext("2d") + const img = new Image() + + img.onload = () => { + canvas.width = size + canvas.height = size + + // 計算裁切區域 (正方形縮圖) + const minDimension = Math.min(img.width, img.height) + const x = (img.width - minDimension) / 2 + const y = (img.height - minDimension) / 2 + + ctx?.drawImage(img, x, y, minDimension, minDimension, 0, 0, size, size) + + resolve(canvas.toDataURL(file.type, 0.7)) + } + + img.src = URL.createObjectURL(file) + }) +} diff --git a/lib/supabase-image-utils.ts b/lib/supabase-image-utils.ts new file mode 100644 index 0000000..5a96ee0 --- /dev/null +++ b/lib/supabase-image-utils.ts @@ -0,0 +1,330 @@ +import { supabase } from "./supabase" + +// Supabase 圖片相關的類型定義 +export interface SupabaseImageFile { + id: string + name: string + size: number + type: string + storage_path: string // Supabase Storage 中的路徑 + public_url: string // 公開訪問 URL + uploaded_at: string +} + +export interface ImageUploadResult { + success: boolean + data?: SupabaseImageFile + error?: string +} + +export interface BatchUploadResult { + successful: SupabaseImageFile[] + failed: Array<{ file: File; error: string }> + total: number +} + +// 圖片上傳服務 +export class SupabaseImageService { + private static readonly BUCKET_NAME = "wish-images" + private static readonly MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB + private static readonly ALLOWED_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"] + + // 驗證圖片文件 + static validateImageFile(file: File): { isValid: boolean; error?: string } { + if (!this.ALLOWED_TYPES.includes(file.type)) { + return { + isValid: false, + error: `不支援的檔案格式: ${file.type}。請使用 JPG、PNG、WebP 或 GIF 格式。`, + } + } + + if (file.size > this.MAX_FILE_SIZE) { + const sizeMB = (file.size / (1024 * 1024)).toFixed(1) + return { + isValid: false, + error: `檔案過大: ${sizeMB}MB。請壓縮圖片至 5MB 以下。`, + } + } + + return { isValid: true } + } + + // 生成唯一的檔案路徑 + static generateFilePath(file: File): string { + const timestamp = Date.now() + const randomId = Math.random().toString(36).substring(2, 15) + const extension = file.name.split(".").pop()?.toLowerCase() || "jpg" + return `${timestamp}_${randomId}.${extension}` + } + + // 上傳單個圖片到 Supabase Storage + static async uploadImage(file: File): Promise { + try { + // 驗證檔案 + const validation = this.validateImageFile(file) + if (!validation.isValid) { + return { success: false, error: validation.error } + } + + // 生成檔案路徑 + const filePath = this.generateFilePath(file) + + // 上傳到 Supabase Storage + const { data: uploadData, error: uploadError } = await supabase.storage + .from(this.BUCKET_NAME) + .upload(filePath, file, { + cacheControl: "3600", + upsert: false, + }) + + if (uploadError) { + console.error("Upload error:", uploadError) + return { success: false, error: `上傳失敗: ${uploadError.message}` } + } + + // 獲取公開 URL + const { data: urlData } = supabase.storage.from(this.BUCKET_NAME).getPublicUrl(filePath) + + if (!urlData.publicUrl) { + return { success: false, error: "無法獲取圖片 URL" } + } + + // 創建圖片記錄 + const imageFile: SupabaseImageFile = { + id: `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + name: file.name, + size: file.size, + type: file.type, + storage_path: filePath, + public_url: urlData.publicUrl, + uploaded_at: new Date().toISOString(), + } + + return { success: true, data: imageFile } + } catch (error) { + console.error("Image upload error:", error) + return { success: false, error: `上傳過程中發生錯誤: ${error}` } + } + } + + // 批量上傳圖片 + static async uploadImages(files: File[]): Promise { + const result: BatchUploadResult = { + successful: [], + failed: [], + total: files.length, + } + + // 並行上傳所有圖片 + const uploadPromises = files.map(async (file) => { + const uploadResult = await this.uploadImage(file) + if (uploadResult.success && uploadResult.data) { + result.successful.push(uploadResult.data) + } else { + result.failed.push({ + file, + error: uploadResult.error || "未知錯誤", + }) + } + }) + + await Promise.all(uploadPromises) + return result + } + + // 刪除圖片 + static async deleteImage(storagePath: string): Promise<{ success: boolean; error?: string }> { + try { + const { error } = await supabase.storage.from(this.BUCKET_NAME).remove([storagePath]) + + if (error) { + console.error("Delete error:", error) + return { success: false, error: `刪除失敗: ${error.message}` } + } + + return { success: true } + } catch (error) { + console.error("Image delete error:", error) + return { success: false, error: `刪除過程中發生錯誤: ${error}` } + } + } + + // 批量刪除圖片 + static async deleteImages(storagePaths: string[]): Promise<{ success: boolean; error?: string }> { + try { + const { error } = await supabase.storage.from(this.BUCKET_NAME).remove(storagePaths) + + if (error) { + console.error("Batch delete error:", error) + return { success: false, error: `批量刪除失敗: ${error.message}` } + } + + return { success: true } + } catch (error) { + console.error("Batch delete error:", error) + return { success: false, error: `批量刪除過程中發生錯誤: ${error}` } + } + } + + // 獲取圖片的公開 URL + static getPublicUrl(storagePath: string): string { + const { data } = supabase.storage.from(this.BUCKET_NAME).getPublicUrl(storagePath) + return data.publicUrl + } + + // 檢查存儲桶是否存在並可訪問 + static async checkStorageHealth(): Promise<{ healthy: boolean; error?: string }> { + try { + const { data, error } = await supabase.storage.from(this.BUCKET_NAME).list("", { limit: 1 }) + + if (error) { + return { healthy: false, error: `存儲檢查失敗: ${error.message}` } + } + + return { healthy: true } + } catch (error) { + return { healthy: false, error: `存儲檢查過程中發生錯誤: ${error}` } + } + } + + // 獲取存儲使用統計 + static async getStorageStats(): Promise<{ + totalFiles: number + totalSize: number + error?: string + }> { + try { + const { data, error } = await supabase.storage.from(this.BUCKET_NAME).list("", { limit: 1000 }) + + if (error) { + return { totalFiles: 0, totalSize: 0, error: error.message } + } + + const totalFiles = data?.length || 0 + const totalSize = data?.reduce((sum, file) => sum + (file.metadata?.size || 0), 0) || 0 + + return { totalFiles, totalSize } + } catch (error) { + return { totalFiles: 0, totalSize: 0, error: `獲取統計失敗: ${error}` } + } + } +} + +// 圖片壓縮工具(在上傳前使用) +export class ImageCompressionService { + // 壓縮圖片 + static async compressImage(file: File, maxWidth = 1920, quality = 0.8): Promise { + return new Promise((resolve) => { + const canvas = document.createElement("canvas") + const ctx = canvas.getContext("2d") + const img = new Image() + + img.onload = () => { + // 計算新尺寸 + let { width, height } = img + + if (width > maxWidth) { + height = (height * maxWidth) / width + width = maxWidth + } + + canvas.width = width + canvas.height = height + + // 繪製壓縮後的圖片 + ctx?.drawImage(img, 0, 0, width, height) + + canvas.toBlob( + (blob) => { + if (blob) { + const compressedFile = new File([blob], file.name, { + type: file.type, + lastModified: Date.now(), + }) + resolve(compressedFile) + } else { + resolve(file) // 如果壓縮失敗,返回原檔案 + } + }, + file.type, + quality, + ) + } + + img.onerror = () => resolve(file) // 如果載入失敗,返回原檔案 + img.src = URL.createObjectURL(file) + }) + } + + // 批量壓縮圖片 + static async compressImages(files: File[]): Promise { + const compressionPromises = files.map((file) => { + // 如果檔案小於 1MB,不需要壓縮 + if (file.size < 1024 * 1024) { + return Promise.resolve(file) + } + return this.compressImage(file, 1920, 0.8) + }) + + return Promise.all(compressionPromises) + } +} + +// 從舊的 base64 格式遷移到 Supabase Storage +export class ImageMigrationService { + // 將 base64 圖片遷移到 Supabase Storage + static async migrateBase64ToStorage(base64Data: string, fileName: string): Promise { + try { + // 將 base64 轉換為 Blob + const response = await fetch(base64Data) + const blob = await response.blob() + + // 創建 File 對象 + const file = new File([blob], fileName, { type: blob.type }) + + // 上傳到 Supabase Storage + return await SupabaseImageService.uploadImage(file) + } catch (error) { + console.error("Base64 migration error:", error) + return { success: false, error: `遷移失敗: ${error}` } + } + } + + // 批量遷移圖片 + static async migrateImagesFromWish(wishImages: any[]): Promise<{ + successful: SupabaseImageFile[] + failed: Array<{ originalImage: any; error: string }> + }> { + const result = { + successful: [] as SupabaseImageFile[], + failed: [] as Array<{ originalImage: any; error: string }>, + } + + for (const image of wishImages) { + try { + if (image.base64) { + // 遷移 base64 圖片 + const migrationResult = await this.migrateBase64ToStorage(image.base64, image.name) + if (migrationResult.success && migrationResult.data) { + result.successful.push(migrationResult.data) + } else { + result.failed.push({ + originalImage: image, + error: migrationResult.error || "遷移失敗", + }) + } + } else if (image.storage_path) { + // 已經是 Supabase Storage 格式,直接保留 + result.successful.push(image as SupabaseImageFile) + } + } catch (error) { + result.failed.push({ + originalImage: image, + error: `處理失敗: ${error}`, + }) + } + } + + return result + } +} diff --git a/lib/supabase-service-updated.ts b/lib/supabase-service-updated.ts new file mode 100644 index 0000000..1355172 --- /dev/null +++ b/lib/supabase-service-updated.ts @@ -0,0 +1,299 @@ +import { supabase, type Database } from "./supabase" +import { SupabaseImageService, ImageMigrationService, type SupabaseImageFile } from "./supabase-image-utils" + +// 更新的 Wish 類型定義 +export type Wish = Database["public"]["Tables"]["wishes"]["Row"] & { + like_count?: number + images?: SupabaseImageFile[] // 使用新的圖片類型 +} + +export type WishInsert = Database["public"]["Tables"]["wishes"]["Insert"] +export type WishLike = Database["public"]["Tables"]["wish_likes"]["Row"] +export type UserSettings = Database["public"]["Tables"]["user_settings"]["Row"] + +// 錯誤處理 +export class SupabaseError extends Error { + constructor( + message: string, + public originalError?: any, + ) { + super(message) + this.name = "SupabaseError" + } +} + +// 更新的困擾案例服務 +export class WishService { + // 獲取所有公開的困擾案例(帶點讚數和圖片) + static async getPublicWishes(): Promise { + try { + const { data, error } = await supabase + .from("wishes_with_likes") + .select("*") + .eq("is_public", true) + .order("created_at", { ascending: false }) + + if (error) throw new SupabaseError("獲取公開困擾失敗", error) + + // 轉換圖片格式 + return (data || []).map((wish) => ({ + ...wish, + images: this.parseImages(wish.images), + })) + } catch (error) { + console.error("Error fetching public wishes:", error) + throw error + } + } + + // 獲取所有困擾案例(用於分析,包含私密的) + static async getAllWishes(): Promise { + try { + const { data, error } = await supabase + .from("wishes_with_likes") + .select("*") + .order("created_at", { ascending: false }) + + if (error) throw new SupabaseError("獲取所有困擾失敗", error) + + // 轉換圖片格式 + return (data || []).map((wish) => ({ + ...wish, + images: this.parseImages(wish.images), + })) + } catch (error) { + console.error("Error fetching all wishes:", error) + throw error + } + } + + // 創建新的困擾案例(支持 Supabase Storage 圖片) + static async createWish(wishData: { + title: string + currentPain: string + expectedSolution: string + expectedEffect?: string + isPublic?: boolean + email?: string + images?: SupabaseImageFile[] + }): Promise { + try { + // 準備圖片數據 + const imageData = + wishData.images?.map((img) => ({ + id: img.id, + name: img.name, + size: img.size, + type: img.type, + storage_path: img.storage_path, + public_url: img.public_url, + uploaded_at: img.uploaded_at, + })) || [] + + const insertData: WishInsert = { + title: wishData.title, + current_pain: wishData.currentPain, + expected_solution: wishData.expectedSolution, + expected_effect: wishData.expectedEffect || null, + is_public: wishData.isPublic ?? true, + email: wishData.email || null, + images: imageData, + } + + const { data, error } = await supabase.from("wishes").insert(insertData).select().single() + + if (error) throw new SupabaseError("創建困擾失敗", error) + + return { + ...data, + images: this.parseImages(data.images), + } + } catch (error) { + console.error("Error creating wish:", error) + throw error + } + } + + // 解析圖片數據 + private static parseImages(imagesData: any): SupabaseImageFile[] { + if (!imagesData || !Array.isArray(imagesData)) return [] + + return imagesData.map((img) => ({ + id: img.id || `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + name: img.name || "unknown.jpg", + size: img.size || 0, + type: img.type || "image/jpeg", + storage_path: img.storage_path || "", + public_url: img.public_url || img.url || "", // 向後兼容 + uploaded_at: img.uploaded_at || new Date().toISOString(), + })) + } + + // 獲取統計數據 + static async getWishesStats() { + try { + const { data, error } = await supabase.rpc("get_wishes_stats") + + if (error) throw new SupabaseError("獲取統計數據失敗", error) + return data + } catch (error) { + console.error("Error fetching wishes stats:", error) + throw error + } + } + + // 刪除困擾案例(包括相關圖片) + static async deleteWish(wishId: number): Promise { + try { + // 先獲取困擾案例的圖片信息 + const { data: wish, error: fetchError } = await supabase.from("wishes").select("images").eq("id", wishId).single() + + if (fetchError) throw new SupabaseError("獲取困擾案例失敗", fetchError) + + // 刪除相關圖片 + if (wish.images && Array.isArray(wish.images)) { + const storagePaths = wish.images.map((img: any) => img.storage_path).filter((path: string) => path) + + if (storagePaths.length > 0) { + await SupabaseImageService.deleteImages(storagePaths) + } + } + + // 刪除困擾案例記錄 + const { error: deleteError } = await supabase.from("wishes").delete().eq("id", wishId) + + if (deleteError) throw new SupabaseError("刪除困擾案例失敗", deleteError) + + return true + } catch (error) { + console.error("Error deleting wish:", error) + throw error + } + } +} + +// 更新的數據遷移服務 +export class MigrationService { + // 遷移 localStorage 中的困擾案例到 Supabase(包括圖片遷移) + static async migrateWishesFromLocalStorage(): Promise<{ + success: number + failed: number + errors: string[] + }> { + const result = { + success: 0, + failed: 0, + errors: [] as string[], + } + + try { + const localWishes = JSON.parse(localStorage.getItem("wishes") || "[]") + + if (localWishes.length === 0) { + console.log("No local wishes to migrate") + return result + } + + console.log(`Starting migration of ${localWishes.length} wishes...`) + + for (const wish of localWishes) { + try { + let migratedImages: SupabaseImageFile[] = [] + + // 遷移圖片(如果有的話) + if (wish.images && Array.isArray(wish.images) && wish.images.length > 0) { + console.log(`Migrating ${wish.images.length} images for wish: ${wish.title}`) + + const imageMigrationResult = await ImageMigrationService.migrateImagesFromWish(wish.images) + + migratedImages = imageMigrationResult.successful + + if (imageMigrationResult.failed.length > 0) { + console.warn(`Failed to migrate ${imageMigrationResult.failed.length} images for wish: ${wish.title}`) + // 記錄圖片遷移失敗,但不阻止整個 wish 的遷移 + result.errors.push(`部分圖片遷移失敗 "${wish.title}": ${imageMigrationResult.failed.length} 張圖片`) + } + } + + // 創建困擾案例 + await WishService.createWish({ + title: wish.title, + currentPain: wish.currentPain, + expectedSolution: wish.expectedSolution, + expectedEffect: wish.expectedEffect, + isPublic: wish.isPublic !== false, + email: wish.email, + images: migratedImages, + }) + + result.success++ + console.log(`Successfully migrated wish: ${wish.title}`) + } catch (error) { + result.failed++ + result.errors.push(`Failed to migrate wish "${wish.title}": ${error}`) + console.error(`Failed to migrate wish "${wish.title}":`, error) + } + } + + console.log(`Migration completed: ${result.success} success, ${result.failed} failed`) + return result + } catch (error) { + console.error("Migration error:", error) + result.errors.push(`Migration process failed: ${error}`) + return result + } + } + + // 清空 localStorage 中的舊數據 + static clearLocalStorageData(): void { + const keysToRemove = ["wishes", "wishLikes", "userLikedWishes"] + keysToRemove.forEach((key) => { + localStorage.removeItem(key) + }) + console.log("Local storage data cleared") + } +} + +// 存儲健康檢查服務 +export class StorageHealthService { + // 檢查 Supabase Storage 健康狀態 + static async checkStorageHealth(): Promise<{ + healthy: boolean + stats?: { totalFiles: number; totalSize: number } + error?: string + }> { + try { + const healthCheck = await SupabaseImageService.checkStorageHealth() + if (!healthCheck.healthy) { + return { healthy: false, error: healthCheck.error } + } + + const stats = await SupabaseImageService.getStorageStats() + return { + healthy: true, + stats: { + totalFiles: stats.totalFiles, + totalSize: stats.totalSize, + }, + error: stats.error, + } + } catch (error) { + return { healthy: false, error: `健康檢查失敗: ${error}` } + } + } + + // 清理孤立的圖片 + static async cleanupOrphanedImages(): Promise<{ cleaned: number; error?: string }> { + try { + const { data, error } = await supabase.rpc("cleanup_orphaned_images") + + if (error) { + return { cleaned: 0, error: error.message } + } + + return { cleaned: data || 0 } + } catch (error) { + return { cleaned: 0, error: `清理過程失敗: ${error}` } + } + } +} diff --git a/lib/supabase-service.ts b/lib/supabase-service.ts new file mode 100644 index 0000000..743ccd8 --- /dev/null +++ b/lib/supabase-service.ts @@ -0,0 +1,322 @@ +import { supabase, getUserSession, type Database } from "./supabase" +import type { ImageFile } from "./image-utils" + +// 類型定義 +export type Wish = Database["public"]["Tables"]["wishes"]["Row"] & { + like_count?: number +} + +export type WishInsert = Database["public"]["Tables"]["wishes"]["Insert"] +export type WishLike = Database["public"]["Tables"]["wish_likes"]["Row"] +export type UserSettings = Database["public"]["Tables"]["user_settings"]["Row"] + +// 錯誤處理 +export class SupabaseError extends Error { + constructor( + message: string, + public originalError?: any, + ) { + super(message) + this.name = "SupabaseError" + } +} + +// 困擾案例相關服務 +export class WishService { + // 獲取所有公開的困擾案例(帶點讚數) + static async getPublicWishes(): Promise { + try { + const { data, error } = await supabase + .from("wishes_with_likes") + .select("*") + .eq("is_public", true) + .order("created_at", { ascending: false }) + + if (error) throw new SupabaseError("獲取公開困擾失敗", error) + return data || [] + } catch (error) { + console.error("Error fetching public wishes:", error) + throw error + } + } + + // 獲取所有困擾案例(用於分析,包含私密的) + static async getAllWishes(): Promise { + try { + const { data, error } = await supabase + .from("wishes_with_likes") + .select("*") + .order("created_at", { ascending: false }) + + if (error) throw new SupabaseError("獲取所有困擾失敗", error) + return data || [] + } catch (error) { + console.error("Error fetching all wishes:", error) + throw error + } + } + + // 創建新的困擾案例 + static async createWish(wishData: { + title: string + currentPain: string + expectedSolution: string + expectedEffect?: string + isPublic?: boolean + email?: string + images?: ImageFile[] + }): Promise { + try { + // 轉換圖片數據格式 + const imageData = + wishData.images?.map((img) => ({ + id: img.id, + name: img.name, + size: img.size, + type: img.type, + base64: img.base64 || img.url, + })) || [] + + const insertData: WishInsert = { + title: wishData.title, + current_pain: wishData.currentPain, + expected_solution: wishData.expectedSolution, + expected_effect: wishData.expectedEffect || null, + is_public: wishData.isPublic ?? true, + email: wishData.email || null, + images: imageData, + } + + const { data, error } = await supabase.from("wishes").insert(insertData).select().single() + + if (error) throw new SupabaseError("創建困擾失敗", error) + return data + } catch (error) { + console.error("Error creating wish:", error) + throw error + } + } + + // 獲取統計數據 + static async getWishesStats() { + try { + const { data, error } = await supabase.rpc("get_wishes_stats") + + if (error) throw new SupabaseError("獲取統計數據失敗", error) + return data + } catch (error) { + console.error("Error fetching wishes stats:", error) + throw error + } + } +} + +// 點讚相關服務 +export class LikeService { + // 為困擾案例點讚 + static async likeWish(wishId: number): Promise { + try { + const userSession = getUserSession() + + const { error } = await supabase.from("wish_likes").insert({ + wish_id: wishId, + user_session: userSession, + }) + + if (error) { + // 如果是重複點讚錯誤,返回 false + if (error.code === "23505") { + return false + } + throw new SupabaseError("點讚失敗", error) + } + + return true + } catch (error) { + console.error("Error liking wish:", error) + throw error + } + } + + // 檢查用戶是否已點讚 + static async hasUserLiked(wishId: number): Promise { + try { + const userSession = getUserSession() + + const { data, error } = await supabase + .from("wish_likes") + .select("id") + .eq("wish_id", wishId) + .eq("user_session", userSession) + .single() + + if (error && error.code !== "PGRST116") { + throw new SupabaseError("檢查點讚狀態失敗", error) + } + + return !!data + } catch (error) { + console.error("Error checking like status:", error) + return false + } + } + + // 獲取困擾案例的點讚數 + static async getWishLikeCount(wishId: number): Promise { + try { + const { count, error } = await supabase + .from("wish_likes") + .select("*", { count: "exact", head: true }) + .eq("wish_id", wishId) + + if (error) throw new SupabaseError("獲取點讚數失敗", error) + return count || 0 + } catch (error) { + console.error("Error fetching like count:", error) + return 0 + } + } + + // 獲取用戶已點讚的困擾 ID 列表 + static async getUserLikedWishes(): Promise { + try { + const userSession = getUserSession() + + const { data, error } = await supabase.from("wish_likes").select("wish_id").eq("user_session", userSession) + + if (error) throw new SupabaseError("獲取用戶點讚記錄失敗", error) + return data?.map((item) => item.wish_id) || [] + } catch (error) { + console.error("Error fetching user liked wishes:", error) + return [] + } + } +} + +// 用戶設定相關服務 +export class UserSettingsService { + // 獲取用戶設定 + static async getUserSettings(): Promise { + try { + const userSession = getUserSession() + + const { data, error } = await supabase.from("user_settings").select("*").eq("user_session", userSession).single() + + if (error && error.code !== "PGRST116") { + throw new SupabaseError("獲取用戶設定失敗", error) + } + + return data + } catch (error) { + console.error("Error fetching user settings:", error) + return null + } + } + + // 更新或創建用戶設定 + static async updateUserSettings(settings: { + backgroundMusicEnabled?: boolean + backgroundMusicVolume?: number + backgroundMusicPlaying?: boolean + }): Promise { + try { + const userSession = getUserSession() + + // 先嘗試更新 + const { data: updateData, error: updateError } = await supabase + .from("user_settings") + .update({ + background_music_enabled: settings.backgroundMusicEnabled, + background_music_volume: settings.backgroundMusicVolume, + background_music_playing: settings.backgroundMusicPlaying, + }) + .eq("user_session", userSession) + .select() + .single() + + if (updateError && updateError.code === "PGRST116") { + // 如果記錄不存在,創建新記錄 + const { data: insertData, error: insertError } = await supabase + .from("user_settings") + .insert({ + user_session: userSession, + background_music_enabled: settings.backgroundMusicEnabled ?? false, + background_music_volume: settings.backgroundMusicVolume ?? 0.3, + background_music_playing: settings.backgroundMusicPlaying ?? false, + }) + .select() + .single() + + if (insertError) throw new SupabaseError("創建用戶設定失敗", insertError) + return insertData + } + + if (updateError) throw new SupabaseError("更新用戶設定失敗", updateError) + return updateData + } catch (error) { + console.error("Error updating user settings:", error) + throw error + } + } +} + +// 數據遷移服務(從 localStorage 遷移到 Supabase) +export class MigrationService { + // 遷移 localStorage 中的困擾案例到 Supabase + static async migrateWishesFromLocalStorage(): Promise<{ + success: number + failed: number + errors: string[] + }> { + const result = { + success: 0, + failed: 0, + errors: [] as string[], + } + + try { + const localWishes = JSON.parse(localStorage.getItem("wishes") || "[]") + + if (localWishes.length === 0) { + console.log("No local wishes to migrate") + return result + } + + console.log(`Starting migration of ${localWishes.length} wishes...`) + + for (const wish of localWishes) { + try { + await WishService.createWish({ + title: wish.title, + currentPain: wish.currentPain, + expectedSolution: wish.expectedSolution, + expectedEffect: wish.expectedEffect, + isPublic: wish.isPublic !== false, // 默認為 true + email: wish.email, + images: wish.images || [], + }) + result.success++ + } catch (error) { + result.failed++ + result.errors.push(`Failed to migrate wish "${wish.title}": ${error}`) + } + } + + console.log(`Migration completed: ${result.success} success, ${result.failed} failed`) + return result + } catch (error) { + console.error("Migration error:", error) + result.errors.push(`Migration process failed: ${error}`) + return result + } + } + + // 清空 localStorage 中的舊數據 + static clearLocalStorageData(): void { + const keysToRemove = ["wishes", "wishLikes", "userLikedWishes"] + keysToRemove.forEach((key) => { + localStorage.removeItem(key) + }) + console.log("Local storage data cleared") + } +} diff --git a/lib/supabase.ts b/lib/supabase.ts new file mode 100644 index 0000000..82e2265 --- /dev/null +++ b/lib/supabase.ts @@ -0,0 +1,152 @@ +import { createClient } from "@supabase/supabase-js" + +// Supabase 配置 +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + +// 創建 Supabase 客戶端(單例模式) +export const supabase = createClient(supabaseUrl, supabaseAnonKey, { + auth: { + persistSession: false, // 我們不需要用戶認證 + }, + db: { + schema: "public", + }, +}) + +// 數據庫類型定義 +export interface Database { + public: { + Tables: { + wishes: { + Row: { + id: number + title: string + current_pain: string + expected_solution: string + expected_effect: string | null + is_public: boolean + email: string | null + images: any[] | null + created_at: string + updated_at: string + } + Insert: { + title: string + current_pain: string + expected_solution: string + expected_effect?: string | null + is_public?: boolean + email?: string | null + images?: any[] | null + } + Update: { + title?: string + current_pain?: string + expected_solution?: string + expected_effect?: string | null + is_public?: boolean + email?: string | null + images?: any[] | null + } + } + wish_likes: { + Row: { + id: number + wish_id: number + user_session: string + created_at: string + } + Insert: { + wish_id: number + user_session: string + } + Update: { + wish_id?: number + user_session?: string + } + } + user_settings: { + Row: { + id: number + user_session: string + background_music_enabled: boolean + background_music_volume: number + background_music_playing: boolean + created_at: string + updated_at: string + } + Insert: { + user_session: string + background_music_enabled?: boolean + background_music_volume?: number + background_music_playing?: boolean + } + Update: { + background_music_enabled?: boolean + background_music_volume?: number + background_music_playing?: boolean + } + } + } + Views: { + wishes_with_likes: { + Row: { + id: number + title: string + current_pain: string + expected_solution: string + expected_effect: string | null + is_public: boolean + email: string | null + images: any[] | null + created_at: string + updated_at: string + like_count: number + } + } + } + Functions: { + get_wishes_stats: { + Args: {} + Returns: { + total_wishes: number + public_wishes: number + private_wishes: number + this_week: number + last_week: number + } + } + } + } +} + +// 生成用戶會話 ID(用於匿名識別) +export function getUserSession(): string { + if (typeof window === "undefined") return "server-session" + + let session = localStorage.getItem("user_session") + if (!session) { + session = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + localStorage.setItem("user_session", session) + } + return session +} + +// 測試 Supabase 連接 +export async function testSupabaseConnection(): Promise { + try { + const { data, error } = await supabase.from("wishes").select("count").limit(1) + + if (error) { + console.error("Supabase connection test failed:", error) + return false + } + + console.log("✅ Supabase connection successful") + return true + } catch (error) { + console.error("Supabase connection test error:", error) + return false + } +} diff --git a/package.json b/package.json index a85b852..674da41 100644 --- a/package.json +++ b/package.json @@ -37,23 +37,26 @@ "@radix-ui/react-toggle": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-tooltip": "1.1.6", + "@supabase/supabase-js": "latest", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", "date-fns": "4.1.0", + "dotenv": "latest", "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", "lucide-react": "^0.454.0", - "next": "15.2.4", + "next": "14.2.16", "next-themes": "^0.4.4", - "react": "^19", + "react": "^18", "react-day-picker": "8.10.1", - "react-dom": "^19", + "react-dom": "^18", "react-hook-form": "^7.54.1", "react-resizable-panels": "^2.1.7", "recharts": "2.15.0", "shadcn": "latest", + "sharp": "^0.34.3", "sonner": "^1.7.1", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", @@ -62,8 +65,8 @@ }, "devDependencies": { "@types/node": "^22", - "@types/react": "^19", - "@types/react-dom": "^19", + "@types/react": "^18", + "@types/react-dom": "^18", "postcss": "^8.5", "tailwindcss": "^3.4.17", "typescript": "^5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca98c48..275b06d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,4 +2,5659 @@ lockfileVersion: '9.0' settings: autoInstallPeers: true - excludeLinksFromLockfile: false \ No newline at end of file + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hookform/resolvers': + specifier: ^3.9.1 + version: 3.10.0(react-hook-form@7.60.0(react@18.3.1)) + '@radix-ui/react-accordion': + specifier: 1.2.2 + version: 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: 1.1.4 + version: 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-aspect-ratio': + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: 1.1.3 + version: 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context-menu': + specifier: 2.2.4 + version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: 1.1.4 + version: 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: 2.1.4 + version: 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': + specifier: 1.1.4 + version: 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: 2.1.1 + version: 2.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menubar': + specifier: 1.1.4 + version: 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': + specifier: 1.2.3 + version: 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: 1.1.4 + version: 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: 1.2.2 + version: 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: 1.2.2 + version: 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: 2.1.4 + version: 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: 1.2.2 + version: 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: 1.1.1 + version: 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-switch': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: 1.2.4 + version: 1.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: 1.1.6 + version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@supabase/supabase-js': + specifier: latest + version: 2.52.0 + autoprefixer: + specifier: ^10.4.20 + version: 10.4.21(postcss@8.5.6) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: 1.0.4 + version: 1.0.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + date-fns: + specifier: 4.1.0 + version: 4.1.0 + dotenv: + specifier: latest + version: 17.2.0 + embla-carousel-react: + specifier: 8.5.1 + version: 8.5.1(react@18.3.1) + input-otp: + specifier: 1.4.1 + version: 1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lucide-react: + specifier: ^0.454.0 + version: 0.454.0(react@18.3.1) + next: + specifier: 14.2.16 + version: 14.2.16(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: + specifier: ^0.4.4 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18 + version: 18.3.1 + react-day-picker: + specifier: 8.10.1 + version: 8.10.1(date-fns@4.1.0)(react@18.3.1) + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.54.1 + version: 7.60.0(react@18.3.1) + react-resizable-panels: + specifier: ^2.1.7 + version: 2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: 2.15.0 + version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + shadcn: + specifier: latest + version: 2.9.2(@types/node@22.16.4)(typescript@5.8.3) + sharp: + specifier: ^0.34.3 + version: 0.34.3 + sonner: + specifier: ^1.7.1 + version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.5.5 + version: 2.6.0 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.17) + vaul: + specifier: ^0.9.6 + version: 0.9.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^22 + version: 22.16.4 + '@types/react': + specifier: ^18 + version: 18.3.23 + '@types/react-dom': + specifier: ^18 + version: 18.3.7(@types/react@18.3.23) + postcss: + specifier: ^8.5 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5 + version: 5.8.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/ni@23.3.1': + resolution: {integrity: sha512-C90iyzm/jLV7Lomv2UzwWUzRv9WZr1oRsFRKsX5HjQL4EXrbi9H/RtBkjCP+NF+ABZXUKpAa4F1dkoTaea4zHg==} + hasBin: true + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + engines: {node: '>=6.9.0'} + + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + + '@emnapi/runtime@1.4.4': + resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + + '@floating-ui/dom@1.7.2': + resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + + '@floating-ui/react-dom@2.1.4': + resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hookform/resolvers@3.10.0': + resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} + peerDependencies: + react-hook-form: ^7.0.0 + + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/confirm@5.1.13': + resolution: {integrity: sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.1.14': + resolution: {integrity: sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.12': + resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.7': + resolution: {integrity: sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@modelcontextprotocol/sdk@1.16.0': + resolution: {integrity: sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==} + engines: {node: '>=18'} + + '@mswjs/interceptors@0.39.3': + resolution: {integrity: sha512-9bw/wBL7pblsnOCIqvn1788S9o4h+cC5HWXg0Xhh0dOzsZ53IyfmBM+FYqpDDPbm0xjCqEqvCITloF3Dm4TXRQ==} + engines: {node: '>=18'} + + '@next/env@14.2.16': + resolution: {integrity: sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==} + + '@next/swc-darwin-arm64@14.2.16': + resolution: {integrity: sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.16': + resolution: {integrity: sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.16': + resolution: {integrity: sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.2.16': + resolution: {integrity: sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.2.16': + resolution: {integrity: sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.2.16': + resolution: {integrity: sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.2.16': + resolution: {integrity: sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.16': + resolution: {integrity: sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.16': + resolution: {integrity: sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/react-accordion@1.2.2': + resolution: {integrity: sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.4': + resolution: {integrity: sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.1': + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.1': + resolution: {integrity: sha512-kNU4FIpcFMBLkOUcgeIteH06/8JLBcYY6Le1iKenDGCYNYFX3TQqCZjzkOsz37h7r94/99GTb7YhEr98ZBJibw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.2': + resolution: {integrity: sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.1.3': + resolution: {integrity: sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.2': + resolution: {integrity: sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.1': + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.4': + resolution: {integrity: sha512-ap4wdGwK52rJxGkwukU1NrnEodsUFQIooANKu+ey7d6raQ2biTcEf8za1zr0mgFHieevRTB2nK4dJeN8pTAZGQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.4': + resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.4': + resolution: {integrity: sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.1': + resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.4': + resolution: {integrity: sha512-QSUUnRA3PQ2UhvoCv3eYvMnCAgGQW+sTu86QPuNb+ZMi+ZENd6UWpiXbcWDQ4AEaKF9KKpCHBeaJz9Rw6lRlaQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.1': + resolution: {integrity: sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.4': + resolution: {integrity: sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.4': + resolution: {integrity: sha512-+KMpi7VAZuB46+1LD7a30zb5IxyzLgC8m8j42gk3N4TUCcViNQdX8FhoH1HDvYiA8quuqcek4R4bYpPn/SY1GA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.3': + resolution: {integrity: sha512-IQWAsQ7dsLIYDrn0WqPU+cdM7MONTv9nqrLVYoie3BPiabSfUVDe6Fr+oEt0Cofsr9ONDcDe9xhmJbL1Uq1yKg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.4': + resolution: {integrity: sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.1': + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.3': + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.1': + resolution: {integrity: sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.2.2': + resolution: {integrity: sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.1': + resolution: {integrity: sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.2': + resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.1.4': + resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.1': + resolution: {integrity: sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.2.2': + resolution: {integrity: sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.1.2': + resolution: {integrity: sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.2': + resolution: {integrity: sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.4': + resolution: {integrity: sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.1': + resolution: {integrity: sha512-OgDLZEA30Ylyz8YSXvnGqIHtERqnUt1KUYTKdw/y8u7Ci6zGiJfXc02jahmcSNK3YcErqioj/9flWC9S1ihfwg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.1': + resolution: {integrity: sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.1.6': + resolution: {integrity: sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.1': + resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + + '@supabase/auth-js@2.71.1': + resolution: {integrity: sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==} + + '@supabase/functions-js@2.4.5': + resolution: {integrity: sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.19.4': + resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==} + + '@supabase/realtime-js@2.11.15': + resolution: {integrity: sha512-HQKRnwAqdVqJW/P9TjKVK+/ETpW4yQ8tyDPPtRMKOH4Uh3vQD74vmj353CYs8+YwVBKubeUOOEpI9CT8mT4obw==} + + '@supabase/storage-js@2.7.1': + resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} + + '@supabase/supabase-js@2.52.0': + resolution: {integrity: sha512-jbs3CV1f2+ge7sgBeEduboT9v/uGjF22v0yWi/5/XFn5tbM8MfWRccsMtsDwAwu24XK8H6wt2LJDiNnZLtx/bg==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@ts-morph/common@0.19.0': + resolution: {integrity: sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/node@22.16.4': + resolution: {integrity: sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==} + + '@types/phoenix@1.6.6': + resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.23': + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.0.4: + resolution: {integrity: sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + code-block-writer@12.0.0: + resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dotenv@17.2.0: + resolution: {integrity: sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.187: + resolution: {integrity: sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==} + + embla-carousel-react@8.5.1: + resolution: {integrity: sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.5.1: + resolution: {integrity: sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==} + peerDependencies: + embla-carousel: 8.5.1 + + embla-carousel@8.5.1: + resolution: {integrity: sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventsource-parser@3.0.3: + resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} + engines: {node: '>=20.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.2.2: + resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@6.2.1: + resolution: {integrity: sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA==} + engines: {node: '>= 14'} + + human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + input-otp@1.4.1: + resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.454.0: + resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + minimatch@7.4.6: + resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@2.1.6: + resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.10.4: + resolution: {integrity: sha512-6R1or/qyele7q3RyPwNuvc0IxO8L8/Aim6Sz5ncXEgcWUNxSKE+udriTOWHtpMwmfkLYlacA2y7TIx4cL5lgHA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@14.2.16: + resolution: {integrity: sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + ora@6.3.1: + resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + 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} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-hook-form@7.60.0: + resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-resizable-panels@2.1.9: + resolution: {integrity: sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + 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 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.0: + resolution: {integrity: sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn@2.9.2: + resolution: {integrity: sha512-Ssat5Qlosk3XQckSmHEUZ1WDiXXxZbeXEl2HI4QKlBwmboMHYFaVhOMl3ObRVN578C/d369AsKQcgLWF8F5hCA==} + hasBin: true + + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sonner@1.7.4: + resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stdin-discarder@0.1.0: + resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-morph@18.0.0: + resolution: {integrity: sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vaul@0.9.9: + resolution: {integrity: sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@antfu/ni@23.3.1': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.1 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.1 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.27.6': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.2 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + + '@emnapi/runtime@1.4.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@floating-ui/core@1.7.2': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.2': + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.10': {} + + '@hookform/resolvers@3.10.0(react-hook-form@7.60.0(react@18.3.1))': + dependencies: + react-hook-form: 7.60.0(react@18.3.1) + + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.4.4 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + + '@img/sharp-win32-ia32@0.34.3': + optional: true + + '@img/sharp-win32-x64@0.34.3': + optional: true + + '@inquirer/confirm@5.1.13(@types/node@22.16.4)': + dependencies: + '@inquirer/core': 10.1.14(@types/node@22.16.4) + '@inquirer/type': 3.0.7(@types/node@22.16.4) + optionalDependencies: + '@types/node': 22.16.4 + + '@inquirer/core@10.1.14(@types/node@22.16.4)': + dependencies: + '@inquirer/figures': 1.0.12 + '@inquirer/type': 3.0.7(@types/node@22.16.4) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 22.16.4 + + '@inquirer/figures@1.0.12': {} + + '@inquirer/type@3.0.7(@types/node@22.16.4)': + optionalDependencies: + '@types/node': 22.16.4 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@modelcontextprotocol/sdk@1.16.0': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@mswjs/interceptors@0.39.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@next/env@14.2.16': {} + + '@next/swc-darwin-arm64@14.2.16': + optional: true + + '@next/swc-darwin-x64@14.2.16': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.16': + optional: true + + '@next/swc-linux-arm64-musl@14.2.16': + optional: true + + '@next/swc-linux-x64-gnu@14.2.16': + optional: true + + '@next/swc-linux-x64-musl@14.2.16': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.16': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.16': + optional: true + + '@next/swc-win32-x64-msvc@14.2.16': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@radix-ui/number@1.1.0': {} + + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/react-accordion@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collapsible': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-alert-dialog@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-arrow@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-aspect-ratio@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-avatar@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-checkbox@1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-collapsible@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-collection@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-context-menu@2.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-menu': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-context@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-dialog@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-direction@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-dropdown-menu@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-menu': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-focus-scope@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-hover-card@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-id@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-label@2.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-menu@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-menubar@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-menu': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-navigation-menu@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-popover@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-popper@1.2.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-portal@1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-progress@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-radio-group@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-roving-focus@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-scroll-area@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-select@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-separator@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-slider@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-slot@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-switch@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-tabs@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-toast@1.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-toggle-group@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-toggle@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-tooltip@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/rect@1.1.0': {} + + '@supabase/auth-js@2.71.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.5': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.19.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.11.15': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.6 + '@types/ws': 8.18.1 + isows: 1.0.7(ws@8.18.3) + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.7.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.52.0': + dependencies: + '@supabase/auth-js': 2.71.1 + '@supabase/functions-js': 2.4.5 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.19.4 + '@supabase/realtime-js': 2.11.15 + '@supabase/storage-js': 2.7.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.8.1 + + '@ts-morph/common@0.19.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 7.4.6 + mkdirp: 2.1.6 + path-browserify: 1.0.1 + + '@types/cookie@0.6.0': {} + + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/node@22.16.4': + dependencies: + undici-types: 6.21.0 + + '@types/phoenix@1.6.6': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.23)': + dependencies: + '@types/react': 18.3.23 + + '@types/react@18.3.23': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@types/statuses@2.0.6': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.16.4 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + caniuse-lite: 1.0.30001727 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + binary-extensions@2.3.0: {} + + bl@5.1.0: + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.187 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001727: {} + + chalk@5.4.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + clsx@2.1.1: {} + + cmdk@1.0.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.5.0(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + code-block-writer@12.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + commander@10.0.1: {} + + commander@4.1.1: {} + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@8.3.6(typescript@5.8.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.8.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + data-uri-to-buffer@4.0.1: {} + + date-fns@4.1.0: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + depd@2.0.0: {} + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + diff@5.2.0: {} + + dlv@1.1.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.27.6 + csstype: 3.1.3 + + dotenv@17.2.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.187: {} + + embla-carousel-react@8.5.1(react@18.3.1): + dependencies: + embla-carousel: 8.5.1 + embla-carousel-reactive-utils: 8.5.1(embla-carousel@8.5.1) + react: 18.3.1 + + embla-carousel-reactive-utils@8.5.1(embla-carousel@8.5.1): + dependencies: + embla-carousel: 8.5.1 + + embla-carousel@8.5.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + esprima@4.0.1: {} + + etag@1.8.1: {} + + eventemitter3@4.0.7: {} + + eventsource-parser@3.0.3: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.3 + + execa@7.2.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-equals@5.2.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fraction.js@4.3.7: {} + + fresh@2.0.0: {} + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-own-enumerable-keys@1.0.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@16.11.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@4.0.3: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + https-proxy-agent@6.2.1: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + human-signals@4.3.1: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inherits@2.0.4: {} + + input-otp@1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + internmap@2.0.3: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@2.0.0: {} + + is-node-process@1.2.0: {} + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-promise@4.0.0: {} + + is-regexp@3.1.0: {} + + is-stream@3.0.0: {} + + is-unicode-supported@1.3.0: {} + + isexe@2.0.0: {} + + isows@1.0.7(ws@8.18.3): + dependencies: + ws: 8.18.3 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json5@2.2.3: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lodash@4.17.21: {} + + log-symbols@5.1.0: + dependencies: + chalk: 5.4.1 + is-unicode-supported: 1.3.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.454.0(react@18.3.1): + dependencies: + react: 18.3.1 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + minimatch@7.4.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mkdirp@2.1.6: {} + + ms@2.1.3: {} + + msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.1.13(@types/node@22.16.4) + '@mswjs/interceptors': 0.39.3 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.6 + graphql: 16.11.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + strict-event-emitter: 0.5.1 + type-fest: 4.41.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + negotiator@1.0.0: {} + + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + next@14.2.16(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.16 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001727 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.28.0)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.16 + '@next/swc-darwin-x64': 14.2.16 + '@next/swc-linux-arm64-gnu': 14.2.16 + '@next/swc-linux-arm64-musl': 14.2.16 + '@next/swc-linux-x64-gnu': 14.2.16 + '@next/swc-linux-x64-musl': 14.2.16 + '@next/swc-win32-arm64-msvc': 14.2.16 + '@next/swc-win32-ia32-msvc': 14.2.16 + '@next/swc-win32-x64-msvc': 14.2.16 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + ora@6.3.1: + dependencies: + chalk: 5.4.1 + cli-cursor: 4.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + stdin-discarder: 0.1.0 + strip-ansi: 7.1.0 + wcwidth: 1.0.1 + + outvariant@1.4.3: {} + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.2.0: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + pkce-challenge@5.0.0: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.10 + + postcss-js@4.0.1(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@4.0.2(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.0 + optionalDependencies: + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + 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 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + react-day-picker@8.10.1(date-fns@4.1.0)(react@18.3.1): + dependencies: + date-fns: 4.1.0 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-hook-form@7.60.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + react-remove-scroll@2.7.1(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.23)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + + react-resizable-panels@2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.2.2 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-style-singleton@2.2.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + require-directory@2.1.1: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.1.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.7.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shadcn@2.9.2(@types/node@22.16.4)(typescript@5.8.3): + dependencies: + '@antfu/ni': 23.3.1 + '@babel/core': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) + '@modelcontextprotocol/sdk': 1.16.0 + commander: 10.0.1 + cosmiconfig: 8.3.6(typescript@5.8.3) + deepmerge: 4.3.1 + diff: 5.2.0 + execa: 7.2.0 + fast-glob: 3.3.3 + fs-extra: 11.3.0 + https-proxy-agent: 6.2.1 + kleur: 4.1.5 + msw: 2.10.4(@types/node@22.16.4)(typescript@5.8.3) + node-fetch: 3.3.2 + ora: 6.3.1 + postcss: 8.5.6 + prompts: 2.4.2 + recast: 0.23.11 + stringify-object: 5.0.0 + ts-morph: 18.0.0 + tsconfig-paths: 4.2.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - '@types/node' + - supports-color + - typescript + + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + sonner@1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + stdin-discarder@0.1.0: + dependencies: + bl: 5.1.0 + + streamsearch@1.1.0: {} + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-object@5.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-final-newline@3.0.0: {} + + styled-jsx@5.1.1(@babel/core@7.28.0)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.28.0 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@2.6.0: {} + + tailwindcss-animate@1.0.7(tailwindcss@3.4.17): + dependencies: + tailwindcss: 3.4.17 + + tailwindcss@3.4.17: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-invariant@1.3.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@0.0.3: {} + + ts-interface-checker@0.1.13: {} + + ts-morph@18.0.0: + dependencies: + '@ts-morph/common': 0.19.0 + code-block-writer: 12.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-sidecar@1.1.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + vary@1.1.2: {} + + vaul@0.9.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-streams-polyfill@3.3.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@2.8.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yoctocolors-cjs@2.1.2: {} + + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/scripts/01-create-tables.sql b/scripts/01-create-tables.sql new file mode 100644 index 0000000..066e7fa --- /dev/null +++ b/scripts/01-create-tables.sql @@ -0,0 +1,118 @@ +-- 心願星河 - 基礎表格創建 +-- 執行順序:第 1 步 +-- 說明:創建應用程式所需的基礎數據表格 + +-- 開始事務 +BEGIN; + +-- 1. 創建 wishes 表格(困擾案例主表) +CREATE TABLE IF NOT EXISTS wishes ( + id BIGSERIAL PRIMARY KEY, + title TEXT NOT NULL CHECK (length(title) >= 1 AND length(title) <= 200), + current_pain TEXT NOT NULL CHECK (length(current_pain) >= 1 AND length(current_pain) <= 2000), + expected_solution TEXT NOT NULL CHECK (length(expected_solution) >= 1 AND length(expected_solution) <= 2000), + expected_effect TEXT CHECK (length(expected_effect) <= 1000), + is_public BOOLEAN DEFAULT true NOT NULL, + email TEXT CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + images JSONB DEFAULT '[]'::jsonb NOT NULL, + user_session TEXT NOT NULL DEFAULT gen_random_uuid()::text, + status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deleted')), + category TEXT CHECK (category IN ('工作效率', '人際關係', '技術問題', '職涯發展', '工作環境', '其他')), + priority INTEGER DEFAULT 3 CHECK (priority >= 1 AND priority <= 5), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +-- 2. 創建 wish_likes 表格(點讚記錄) +CREATE TABLE IF NOT EXISTS wish_likes ( + id BIGSERIAL PRIMARY KEY, + wish_id BIGINT NOT NULL REFERENCES wishes(id) ON DELETE CASCADE, + user_session TEXT NOT NULL, + ip_address INET, + user_agent TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE(wish_id, user_session) +); + +-- 3. 創建 user_settings 表格(用戶設定) +CREATE TABLE IF NOT EXISTS user_settings ( + id BIGSERIAL PRIMARY KEY, + user_session TEXT UNIQUE NOT NULL, + background_music_enabled BOOLEAN DEFAULT false NOT NULL, + background_music_volume DECIMAL(3,2) DEFAULT 0.30 CHECK (background_music_volume >= 0 AND background_music_volume <= 1), + background_music_playing BOOLEAN DEFAULT false NOT NULL, + theme_preference TEXT DEFAULT 'auto' CHECK (theme_preference IN ('light', 'dark', 'auto')), + language_preference TEXT DEFAULT 'zh-TW' CHECK (language_preference IN ('zh-TW', 'en-US')), + notification_enabled BOOLEAN DEFAULT true NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +-- 4. 創建 migration_log 表格(遷移記錄) +CREATE TABLE IF NOT EXISTS migration_log ( + id BIGSERIAL PRIMARY KEY, + user_session TEXT NOT NULL, + migration_type TEXT NOT NULL CHECK (migration_type IN ('wishes', 'likes', 'settings')), + source_data JSONB, + target_records INTEGER DEFAULT 0, + success BOOLEAN DEFAULT false NOT NULL, + error_message TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +-- 5. 創建 system_stats 表格(系統統計) +CREATE TABLE IF NOT EXISTS system_stats ( + id BIGSERIAL PRIMARY KEY, + stat_date DATE DEFAULT CURRENT_DATE NOT NULL, + total_wishes INTEGER DEFAULT 0, + public_wishes INTEGER DEFAULT 0, + private_wishes INTEGER DEFAULT 0, + total_likes INTEGER DEFAULT 0, + active_users INTEGER DEFAULT 0, + storage_used_mb DECIMAL(10,2) DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE(stat_date) +); + +-- 提交事務 +COMMIT; + +-- 添加表格註釋 +COMMENT ON TABLE wishes IS '用戶提交的工作困擾案例主表'; +COMMENT ON TABLE wish_likes IS '困擾案例的點讚記錄表'; +COMMENT ON TABLE user_settings IS '用戶個人設定表(音樂、主題等)'; +COMMENT ON TABLE migration_log IS '數據遷移記錄表'; +COMMENT ON TABLE system_stats IS '系統統計數據表'; + +-- 添加欄位註釋 +COMMENT ON COLUMN wishes.title IS '困擾案例標題'; +COMMENT ON COLUMN wishes.current_pain IS '目前遇到的困擾描述'; +COMMENT ON COLUMN wishes.expected_solution IS '期望的解決方案'; +COMMENT ON COLUMN wishes.expected_effect IS '預期效果描述'; +COMMENT ON COLUMN wishes.is_public IS '是否公開顯示'; +COMMENT ON COLUMN wishes.images IS '相關圖片資訊(JSON格式)'; +COMMENT ON COLUMN wishes.user_session IS '用戶會話標識'; +COMMENT ON COLUMN wishes.category IS '困擾類別'; +COMMENT ON COLUMN wishes.priority IS '優先級(1-5,5最高)'; + +COMMENT ON COLUMN wish_likes.wish_id IS '被點讚的困擾案例ID'; +COMMENT ON COLUMN wish_likes.user_session IS '點讚用戶的會話標識'; +COMMENT ON COLUMN wish_likes.ip_address IS '點讚用戶的IP地址'; + +COMMENT ON COLUMN user_settings.background_music_enabled IS '背景音樂是否啟用'; +COMMENT ON COLUMN user_settings.background_music_volume IS '背景音樂音量(0-1)'; +COMMENT ON COLUMN user_settings.theme_preference IS '主題偏好設定'; + +-- 顯示創建結果 +DO $$ +BEGIN + RAISE NOTICE '✅ 基礎表格創建完成!'; + RAISE NOTICE '📊 創建的表格:'; + RAISE NOTICE ' - wishes(困擾案例)'; + RAISE NOTICE ' - wish_likes(點讚記錄)'; + RAISE NOTICE ' - user_settings(用戶設定)'; + RAISE NOTICE ' - migration_log(遷移記錄)'; + RAISE NOTICE ' - system_stats(系統統計)'; + RAISE NOTICE ''; + RAISE NOTICE '🔄 下一步:執行 02-create-indexes.sql'; +END $$; diff --git a/scripts/02-create-indexes.sql b/scripts/02-create-indexes.sql new file mode 100644 index 0000000..dc86d8f --- /dev/null +++ b/scripts/02-create-indexes.sql @@ -0,0 +1,174 @@ +-- 心願星河 - 索引和觸發器創建 +-- 執行順序:第 2 步 +-- 說明:創建性能優化索引和自動更新觸發器 + +-- 開始事務 +BEGIN; + +-- 1. wishes 表格索引 +CREATE INDEX IF NOT EXISTS idx_wishes_created_at ON wishes(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_wishes_is_public ON wishes(is_public) WHERE is_public = true; +CREATE INDEX IF NOT EXISTS idx_wishes_status ON wishes(status) WHERE status = 'active'; +CREATE INDEX IF NOT EXISTS idx_wishes_category ON wishes(category) WHERE category IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_wishes_priority ON wishes(priority DESC); +CREATE INDEX IF NOT EXISTS idx_wishes_user_session ON wishes(user_session); +CREATE INDEX IF NOT EXISTS idx_wishes_email ON wishes(email) WHERE email IS NOT NULL; + +-- 全文搜索索引 (使用 simple 配置以支持多语言) +CREATE INDEX IF NOT EXISTS idx_wishes_search ON wishes USING gin( + to_tsvector('simple', title || ' ' || current_pain || ' ' || expected_solution) +); + +-- 2. wish_likes 表格索引 +CREATE INDEX IF NOT EXISTS idx_wish_likes_wish_id ON wish_likes(wish_id); +CREATE INDEX IF NOT EXISTS idx_wish_likes_user_session ON wish_likes(user_session); +CREATE INDEX IF NOT EXISTS idx_wish_likes_created_at ON wish_likes(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_wish_likes_ip_address ON wish_likes(ip_address); + +-- 3. user_settings 表格索引 +CREATE INDEX IF NOT EXISTS idx_user_settings_session ON user_settings(user_session); +CREATE INDEX IF NOT EXISTS idx_user_settings_updated_at ON user_settings(updated_at DESC); + +-- 4. migration_log 表格索引 +CREATE INDEX IF NOT EXISTS idx_migration_log_user_session ON migration_log(user_session); +CREATE INDEX IF NOT EXISTS idx_migration_log_type ON migration_log(migration_type); +CREATE INDEX IF NOT EXISTS idx_migration_log_success ON migration_log(success); +CREATE INDEX IF NOT EXISTS idx_migration_log_created_at ON migration_log(created_at DESC); + +-- 5. system_stats 表格索引 +CREATE INDEX IF NOT EXISTS idx_system_stats_date ON system_stats(stat_date DESC); + +-- 6. 創建更新時間觸發器函數 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- 7. 為需要的表格添加更新時間觸發器 +DROP TRIGGER IF EXISTS update_wishes_updated_at ON wishes; +CREATE TRIGGER update_wishes_updated_at + BEFORE UPDATE ON wishes + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_user_settings_updated_at ON user_settings; +CREATE TRIGGER update_user_settings_updated_at + BEFORE UPDATE ON user_settings + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 8. 創建統計更新觸發器函數 +CREATE OR REPLACE FUNCTION update_system_stats() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO system_stats ( + stat_date, + total_wishes, + public_wishes, + private_wishes, + total_likes, + active_users + ) + SELECT + CURRENT_DATE, + COUNT(*) as total_wishes, + COUNT(*) FILTER (WHERE is_public = true) as public_wishes, + COUNT(*) FILTER (WHERE is_public = false) as private_wishes, + (SELECT COUNT(*) FROM wish_likes) as total_likes, + COUNT(DISTINCT user_session) as active_users + FROM wishes + WHERE status = 'active' + ON CONFLICT (stat_date) + DO UPDATE SET + total_wishes = EXCLUDED.total_wishes, + public_wishes = EXCLUDED.public_wishes, + private_wishes = EXCLUDED.private_wishes, + total_likes = EXCLUDED.total_likes, + active_users = EXCLUDED.active_users; + + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +-- 9. 為 wishes 和 wish_likes 添加統計更新觸發器 +DROP TRIGGER IF EXISTS update_stats_on_wish_change ON wishes; +CREATE TRIGGER update_stats_on_wish_change + AFTER INSERT OR UPDATE OR DELETE ON wishes + FOR EACH STATEMENT + EXECUTE FUNCTION update_system_stats(); + +DROP TRIGGER IF EXISTS update_stats_on_like_change ON wish_likes; +CREATE TRIGGER update_stats_on_like_change + AFTER INSERT OR DELETE ON wish_likes + FOR EACH STATEMENT + EXECUTE FUNCTION update_system_stats(); + +-- 10. 創建圖片清理觸發器函數 +CREATE OR REPLACE FUNCTION cleanup_wish_images() +RETURNS TRIGGER AS $$ +BEGIN + -- 當 wish 被刪除時,記錄需要清理的圖片 + IF TG_OP = 'DELETE' THEN + INSERT INTO migration_log ( + user_session, + migration_type, + source_data, + success, + error_message + ) VALUES ( + OLD.user_session, + 'image_cleanup', + OLD.images, + false, + 'Images marked for cleanup' + ); + RETURN OLD; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- 11. 為 wishes 添加圖片清理觸發器 +DROP TRIGGER IF EXISTS cleanup_images_on_wish_delete ON wishes; +CREATE TRIGGER cleanup_images_on_wish_delete + AFTER DELETE ON wishes + FOR EACH ROW + EXECUTE FUNCTION cleanup_wish_images(); + +-- 提交事務 +COMMIT; + +-- 顯示創建結果 +DO $$ +DECLARE + index_count INTEGER; + trigger_count INTEGER; +BEGIN + -- 計算索引數量 + SELECT COUNT(*) INTO index_count + FROM pg_indexes + WHERE schemaname = 'public' + AND indexname LIKE 'idx_%'; + + -- 計算觸發器數量 + SELECT COUNT(*) INTO trigger_count + FROM pg_trigger + WHERE tgname LIKE '%wish%' OR tgname LIKE '%update%'; + + RAISE NOTICE '✅ 索引和觸發器創建完成!'; + RAISE NOTICE '📊 創建統計:'; + RAISE NOTICE ' - 性能索引:% 個', index_count; + RAISE NOTICE ' - 自動觸發器:% 個', trigger_count; + RAISE NOTICE ''; + RAISE NOTICE '🚀 性能優化功能:'; + RAISE NOTICE ' - 快速查詢索引'; + RAISE NOTICE ' - 全文搜索支援'; + RAISE NOTICE ' - 自動統計更新'; + RAISE NOTICE ' - 圖片清理追蹤'; + RAISE NOTICE ''; + RAISE NOTICE '🔄 下一步:執行 03-create-views-functions.sql'; +END $$; diff --git a/scripts/03-create-views-functions.sql b/scripts/03-create-views-functions.sql new file mode 100644 index 0000000..a1091db --- /dev/null +++ b/scripts/03-create-views-functions.sql @@ -0,0 +1,376 @@ +-- 心願星河 - 視圖和函數創建 +-- 執行順序:第 3 步 +-- 說明:創建便利視圖和業務邏輯函數 + +-- 開始事務 +BEGIN; + +-- 1. 創建帶點讚數的困擾案例視圖 +CREATE OR REPLACE VIEW wishes_with_likes AS +SELECT + w.*, + COALESCE(like_counts.like_count, 0) as like_count, + CASE + WHEN w.created_at >= NOW() - INTERVAL '24 hours' THEN 'new' + WHEN like_counts.like_count >= 10 THEN 'popular' + WHEN w.priority >= 4 THEN 'urgent' + ELSE 'normal' + END as badge_type +FROM wishes w +LEFT JOIN ( + SELECT + wish_id, + COUNT(*) as like_count + FROM wish_likes + GROUP BY wish_id +) like_counts ON w.id = like_counts.wish_id +WHERE w.status = 'active'; + +-- 2. 創建公開困擾案例視圖 +CREATE OR REPLACE VIEW public_wishes AS +SELECT * +FROM wishes_with_likes +WHERE is_public = true +ORDER BY created_at DESC; + +-- 3. 創建熱門困擾案例視圖 +CREATE OR REPLACE VIEW popular_wishes AS +SELECT * +FROM wishes_with_likes +WHERE is_public = true +AND like_count >= 3 +ORDER BY like_count DESC, created_at DESC; + +-- 4. 創建統計摘要視圖 +CREATE OR REPLACE VIEW wishes_summary AS +SELECT + COUNT(*) as total_wishes, + COUNT(*) FILTER (WHERE is_public = true) as public_wishes, + COUNT(*) FILTER (WHERE is_public = false) as private_wishes, + COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '7 days') as this_week, + COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '14 days' AND created_at < NOW() - INTERVAL '7 days') as last_week, + COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '24 hours') as today, + AVG(COALESCE(like_counts.like_count, 0))::DECIMAL(10,2) as avg_likes, + COUNT(DISTINCT user_session) as unique_users +FROM wishes w +LEFT JOIN ( + SELECT wish_id, COUNT(*) as like_count + FROM wish_likes + GROUP BY wish_id +) like_counts ON w.id = like_counts.wish_id +WHERE w.status = 'active'; + +-- 5. 創建類別統計視圖 +CREATE OR REPLACE VIEW category_stats AS +SELECT + COALESCE(category, '未分類') as category, + COUNT(*) as wish_count, + COUNT(*) FILTER (WHERE is_public = true) as public_count, + AVG(COALESCE(like_counts.like_count, 0))::DECIMAL(10,2) as avg_likes, + MAX(created_at) as latest_wish +FROM wishes w +LEFT JOIN ( + SELECT wish_id, COUNT(*) as like_count + FROM wish_likes + GROUP BY wish_id +) like_counts ON w.id = like_counts.wish_id +WHERE w.status = 'active' +GROUP BY category +ORDER BY wish_count DESC; + +-- 6. 創建獲取統計數據的函數 +CREATE OR REPLACE FUNCTION get_wishes_stats() +RETURNS JSON AS $$ +DECLARE + result JSON; +BEGIN + SELECT json_build_object( + 'summary', (SELECT row_to_json(wishes_summary.*) FROM wishes_summary), + 'categories', ( + SELECT json_agg(row_to_json(category_stats.*)) + FROM category_stats + ), + 'recent_activity', ( + SELECT json_agg( + json_build_object( + 'date', date_trunc('day', created_at), + 'count', count(*) + ) + ) + FROM wishes + WHERE created_at >= NOW() - INTERVAL '30 days' + AND status = 'active' + GROUP BY date_trunc('day', created_at) + ORDER BY date_trunc('day', created_at) DESC + LIMIT 30 + ) + ) INTO result; + + RETURN result; +END; +$$ LANGUAGE plpgsql; + +-- 7. 創建搜索困擾案例的函數 +CREATE OR REPLACE FUNCTION search_wishes( + search_query TEXT, + limit_count INTEGER DEFAULT 20, + offset_count INTEGER DEFAULT 0 +) +RETURNS TABLE( + id BIGINT, + title TEXT, + current_pain TEXT, + expected_solution TEXT, + like_count BIGINT, + created_at TIMESTAMP WITH TIME ZONE, + relevance REAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + w.id, + w.title, + w.current_pain, + w.expected_solution, + COALESCE(like_counts.like_count, 0) as like_count, + w.created_at, + ts_rank( + to_tsvector('chinese', w.title || ' ' || w.current_pain || ' ' || w.expected_solution), + plainto_tsquery('chinese', search_query) + ) as relevance + FROM wishes w + LEFT JOIN ( + SELECT wish_id, COUNT(*) as like_count + FROM wish_likes + GROUP BY wish_id + ) like_counts ON w.id = like_counts.wish_id + WHERE w.status = 'active' + AND w.is_public = true + AND ( + to_tsvector('chinese', w.title || ' ' || w.current_pain || ' ' || w.expected_solution) + @@ plainto_tsquery('chinese', search_query) + ) + ORDER BY relevance DESC, like_count DESC, w.created_at DESC + LIMIT limit_count + OFFSET offset_count; +END; +$$ LANGUAGE plpgsql; + +-- 8. 創建獲取用戶統計的函數 +CREATE OR REPLACE FUNCTION get_user_stats(session_id TEXT) +RETURNS JSON AS $$ +DECLARE + result JSON; +BEGIN + SELECT json_build_object( + 'total_wishes', ( + SELECT COUNT(*) + FROM wishes + WHERE user_session = session_id AND status = 'active' + ), + 'total_likes_received', ( + SELECT COALESCE(SUM(like_counts.like_count), 0) + FROM wishes w + LEFT JOIN ( + SELECT wish_id, COUNT(*) as like_count + FROM wish_likes + GROUP BY wish_id + ) like_counts ON w.id = like_counts.wish_id + WHERE w.user_session = session_id AND w.status = 'active' + ), + 'total_likes_given', ( + SELECT COUNT(*) + FROM wish_likes + WHERE user_session = session_id + ), + 'recent_wishes', ( + SELECT json_agg( + json_build_object( + 'id', id, + 'title', title, + 'created_at', created_at, + 'like_count', COALESCE(like_counts.like_count, 0) + ) + ) + FROM wishes w + LEFT JOIN ( + SELECT wish_id, COUNT(*) as like_count + FROM wish_likes + GROUP BY wish_id + ) like_counts ON w.id = like_counts.wish_id + WHERE w.user_session = session_id + AND w.status = 'active' + ORDER BY w.created_at DESC + LIMIT 5 + ) + ) INTO result; + + RETURN result; +END; +$$ LANGUAGE plpgsql; + +-- 9. 創建清理孤立圖片的函數 +CREATE OR REPLACE FUNCTION cleanup_orphaned_images() +RETURNS INTEGER AS $$ +DECLARE + deleted_count INTEGER := 0; + image_record RECORD; +BEGIN + -- 記錄清理開始 + INSERT INTO migration_log ( + user_session, + migration_type, + success, + error_message + ) VALUES ( + 'system', + 'image_cleanup', + false, + 'Starting orphaned image cleanup' + ); + + -- 這裡只是標記,實際的 Storage 清理需要在應用層面處理 + -- 因為 SQL 無法直接操作 Supabase Storage + + -- 找出需要清理的圖片記錄 + FOR image_record IN + SELECT DISTINCT jsonb_array_elements(images)->>'storage_path' as image_path + FROM wishes + WHERE status = 'deleted' + AND images IS NOT NULL + AND jsonb_array_length(images) > 0 + LOOP + -- 標記為需要清理 + INSERT INTO migration_log ( + user_session, + migration_type, + source_data, + success, + error_message + ) VALUES ( + 'system', + 'image_cleanup', + json_build_object('image_path', image_record.image_path), + false, + 'Image marked for cleanup: ' || image_record.image_path + ); + + deleted_count := deleted_count + 1; + END LOOP; + + -- 記錄清理完成 + INSERT INTO migration_log ( + user_session, + migration_type, + target_records, + success, + error_message + ) VALUES ( + 'system', + 'image_cleanup', + deleted_count, + true, + 'Orphaned image cleanup completed. Marked ' || deleted_count || ' images for cleanup.' + ); + + RETURN deleted_count; +END; +$$ LANGUAGE plpgsql; + +-- 10. 創建性能檢查函數 +CREATE OR REPLACE FUNCTION get_performance_stats() +RETURNS JSON AS $$ +DECLARE + result JSON; +BEGIN + SELECT json_build_object( + 'table_sizes', ( + SELECT json_object_agg( + table_name, + pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) + ) + FROM ( + SELECT 'wishes' as table_name + UNION SELECT 'wish_likes' + UNION SELECT 'user_settings' + UNION SELECT 'migration_log' + UNION SELECT 'system_stats' + ) tables + ), + 'index_usage', ( + SELECT json_object_agg( + indexname, + json_build_object( + 'size', pg_size_pretty(pg_relation_size(indexname::regclass)), + 'scans', idx_scan, + 'tuples_read', idx_tup_read, + 'tuples_fetched', idx_tup_fetch + ) + ) + FROM pg_stat_user_indexes + WHERE schemaname = 'public' + AND indexname LIKE 'idx_%' + ), + 'query_performance', ( + SELECT json_build_object( + 'avg_query_time', COALESCE(AVG(mean_exec_time), 0), + 'total_queries', COALESCE(SUM(calls), 0), + 'slowest_queries', ( + SELECT json_agg( + json_build_object( + 'query', LEFT(query, 100) || '...', + 'avg_time', mean_exec_time, + 'calls', calls + ) + ) + FROM pg_stat_statements + WHERE query LIKE '%wishes%' + ORDER BY mean_exec_time DESC + LIMIT 5 + ) + ) + FROM pg_stat_statements + WHERE query LIKE '%wishes%' + ) + ) INTO result; + + RETURN result; +END; +$$ LANGUAGE plpgsql; + +-- 提交事務 +COMMIT; + +-- 顯示創建結果 +DO $$ +DECLARE + view_count INTEGER; + function_count INTEGER; +BEGIN + -- 計算視圖數量 + SELECT COUNT(*) INTO view_count + FROM pg_views + WHERE schemaname = 'public'; + + -- 計算函數數量 + SELECT COUNT(*) INTO function_count + FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = 'public' + AND p.proname LIKE '%wish%' OR p.proname LIKE 'get_%' OR p.proname LIKE 'cleanup_%'; + + RAISE NOTICE '✅ 視圖和函數創建完成!'; + RAISE NOTICE '📊 創建統計:'; + RAISE NOTICE ' - 便利視圖:% 個', view_count; + RAISE NOTICE ' - 業務函數:% 個', function_count; + RAISE NOTICE ''; + RAISE NOTICE '🎯 主要功能:'; + RAISE NOTICE ' - wishes_with_likes(帶點讚數的困擾案例)'; + RAISE NOTICE ' - public_wishes(公開困擾案例)'; + RAISE NOTICE ' - popular_wishes(熱門困擾案例)'; + RAISE NOTICE ' - search_wishes()(全文搜索)'; + RAISE NOTICE ' - get_wishes_stats()(統計數據)'; + RAISE NOTICE ' - cleanup_orphaned_images()(圖片清理)'; + RAISE NOTICE ''; + RAISE NOTICE '🔄 下一步:執行 04-setup-storage.sql'; +END $$; diff --git a/scripts/04-setup-storage.sql b/scripts/04-setup-storage.sql new file mode 100644 index 0000000..4210723 --- /dev/null +++ b/scripts/04-setup-storage.sql @@ -0,0 +1,284 @@ +-- 心願星河 - 存儲服務設置 +-- 執行順序:第 4 步 +-- 說明:設置 Supabase Storage 桶和相關政策 + +-- 注意:此腳本需要在 Supabase Dashboard 的 SQL Editor 中執行 +-- 某些 Storage 操作可能需要 service_role 權限 + +-- 開始事務 +BEGIN; + +-- 1. 創建主要圖片存儲桶 +INSERT INTO storage.buckets ( + id, + name, + public, + file_size_limit, + allowed_mime_types, + avif_autodetection +) VALUES ( + 'wish-images', + 'wish-images', + true, + 5242880, -- 5MB + ARRAY['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'], + true +) ON CONFLICT (id) DO UPDATE SET + file_size_limit = EXCLUDED.file_size_limit, + allowed_mime_types = EXCLUDED.allowed_mime_types, + avif_autodetection = EXCLUDED.avif_autodetection; + +-- 2. 創建縮圖存儲桶 +INSERT INTO storage.buckets ( + id, + name, + public, + file_size_limit, + allowed_mime_types, + avif_autodetection +) VALUES ( + 'wish-thumbnails', + 'wish-thumbnails', + true, + 1048576, -- 1MB + ARRAY['image/jpeg', 'image/jpg', 'image/png', 'image/webp'], + true +) ON CONFLICT (id) DO UPDATE SET + file_size_limit = EXCLUDED.file_size_limit, + allowed_mime_types = EXCLUDED.allowed_mime_types, + avif_autodetection = EXCLUDED.avif_autodetection; + +-- 3. 創建存儲使用統計表 +CREATE TABLE IF NOT EXISTS storage_usage ( + id BIGSERIAL PRIMARY KEY, + bucket_name TEXT NOT NULL, + total_files INTEGER DEFAULT 0, + total_size_bytes BIGINT DEFAULT 0, + last_cleanup_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + UNIQUE(bucket_name) +); + +-- 4. 插入初始存儲統計記錄 +INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes) +VALUES + ('wish-images', 0, 0), + ('wish-thumbnails', 0, 0) +ON CONFLICT (bucket_name) DO NOTHING; + +-- 5. 創建存儲統計更新函數 +CREATE OR REPLACE FUNCTION update_storage_usage() +RETURNS VOID AS $$ +BEGIN + -- 更新 wish-images 桶統計 + INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes, updated_at) + SELECT + 'wish-images', + COUNT(*), + COALESCE(SUM(metadata->>'size')::BIGINT, 0), + NOW() + FROM storage.objects + WHERE bucket_id = 'wish-images' + ON CONFLICT (bucket_name) + DO UPDATE SET + total_files = EXCLUDED.total_files, + total_size_bytes = EXCLUDED.total_size_bytes, + updated_at = EXCLUDED.updated_at; + + -- 更新 wish-thumbnails 桶統計 + INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes, updated_at) + SELECT + 'wish-thumbnails', + COUNT(*), + COALESCE(SUM(metadata->>'size')::BIGINT, 0), + NOW() + FROM storage.objects + WHERE bucket_id = 'wish-thumbnails' + ON CONFLICT (bucket_name) + DO UPDATE SET + total_files = EXCLUDED.total_files, + total_size_bytes = EXCLUDED.total_size_bytes, + updated_at = EXCLUDED.updated_at; +END; +$$ LANGUAGE plpgsql; + +-- 6. 創建存儲清理記錄表 +CREATE TABLE IF NOT EXISTS storage_cleanup_log ( + id BIGSERIAL PRIMARY KEY, + bucket_name TEXT NOT NULL, + file_path TEXT NOT NULL, + file_size BIGINT, + cleanup_reason TEXT, + cleanup_status TEXT DEFAULT 'pending' CHECK (cleanup_status IN ('pending', 'completed', 'failed')), + error_message TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + completed_at TIMESTAMP WITH TIME ZONE +); + +-- 7. 創建獲取存儲統計的函數 +CREATE OR REPLACE FUNCTION get_storage_stats() +RETURNS JSON AS $$ +DECLARE + result JSON; +BEGIN + -- 更新統計數據 + PERFORM update_storage_usage(); + + SELECT json_build_object( + 'buckets', ( + SELECT json_agg( + json_build_object( + 'name', bucket_name, + 'total_files', total_files, + 'total_size_mb', ROUND(total_size_bytes / 1024.0 / 1024.0, 2), + 'last_updated', updated_at + ) + ) + FROM storage_usage + ), + 'cleanup_pending', ( + SELECT COUNT(*) + FROM storage_cleanup_log + WHERE cleanup_status = 'pending' + ), + 'total_storage_mb', ( + SELECT ROUND(SUM(total_size_bytes) / 1024.0 / 1024.0, 2) + FROM storage_usage + ) + ) INTO result; + + RETURN result; +END; +$$ LANGUAGE plpgsql; + +-- 8. 創建標記孤立圖片的函數 +CREATE OR REPLACE FUNCTION mark_orphaned_images_for_cleanup() +RETURNS INTEGER AS $$ +DECLARE + marked_count INTEGER := 0; + image_record RECORD; + referenced_images TEXT[]; +BEGIN + -- 獲取所有被引用的圖片路徑 + SELECT ARRAY_AGG(DISTINCT image_path) INTO referenced_images + FROM ( + SELECT jsonb_array_elements_text( + jsonb_path_query_array(images, '$[*].storage_path') + ) as image_path + FROM wishes + WHERE status = 'active' + AND images IS NOT NULL + AND jsonb_array_length(images) > 0 + ) referenced; + + -- 標記孤立的圖片 + FOR image_record IN + SELECT name, metadata->>'size' as file_size + FROM storage.objects + WHERE bucket_id IN ('wish-images', 'wish-thumbnails') + AND (referenced_images IS NULL OR name != ALL(referenced_images)) + LOOP + INSERT INTO storage_cleanup_log ( + bucket_name, + file_path, + file_size, + cleanup_reason, + cleanup_status + ) VALUES ( + CASE + WHEN image_record.name LIKE '%/thumbnails/%' THEN 'wish-thumbnails' + ELSE 'wish-images' + END, + image_record.name, + image_record.file_size::BIGINT, + 'Orphaned image - not referenced by any active wish', + 'pending' + ) ON CONFLICT DO NOTHING; + + marked_count := marked_count + 1; + END LOOP; + + -- 記錄清理操作 + INSERT INTO migration_log ( + user_session, + migration_type, + target_records, + success, + error_message + ) VALUES ( + 'system', + 'storage_cleanup', + marked_count, + true, + 'Marked ' || marked_count || ' orphaned images for cleanup' + ); + + RETURN marked_count; +END; +$$ LANGUAGE plpgsql; + +-- 9. 創建存儲使用量更新觸發器 +CREATE OR REPLACE FUNCTION trigger_storage_usage_update() +RETURNS TRIGGER AS $$ +BEGIN + -- 異步更新存儲統計(避免阻塞主要操作) + PERFORM pg_notify('storage_usage_update', 'update_needed'); + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; + +-- 10. 為 wishes 表添加存儲使用量更新觸發器 +DROP TRIGGER IF EXISTS update_storage_on_wish_change ON wishes; +CREATE TRIGGER update_storage_on_wish_change + AFTER INSERT OR UPDATE OR DELETE ON wishes + FOR EACH STATEMENT + EXECUTE FUNCTION trigger_storage_usage_update(); + +-- 提交事務 +COMMIT; + +-- 顯示創建結果 +DO $$ +DECLARE + bucket_count INTEGER; + storage_table_count INTEGER; +BEGIN + -- 計算存儲桶數量 + SELECT COUNT(*) INTO bucket_count + FROM storage.buckets + WHERE id LIKE 'wish-%'; + + -- 計算存儲相關表格數量 + SELECT COUNT(*) INTO storage_table_count + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE '%storage%'; + + RAISE NOTICE '✅ 存儲服務設置完成!'; + RAISE NOTICE '📊 創建統計:'; + RAISE NOTICE ' - 存儲桶:% 個', bucket_count; + RAISE NOTICE ' - 存儲管理表:% 個', storage_table_count; + RAISE NOTICE ''; + RAISE NOTICE '🗂️ 存儲桶配置:'; + RAISE NOTICE ' - wish-images(主圖片,5MB限制)'; + RAISE NOTICE ' - wish-thumbnails(縮圖,1MB限制)'; + RAISE NOTICE ''; + RAISE NOTICE '🛠️ 管理功能:'; + RAISE NOTICE ' - 自動統計更新'; + RAISE NOTICE ' - 孤立圖片檢測'; + RAISE NOTICE ' - 清理記錄追蹤'; + RAISE NOTICE ''; + RAISE NOTICE '🔄 下一步:執行 05-setup-rls.sql'; +END $$; + +-- 重要提醒 +DO $$ +BEGIN + RAISE NOTICE ''; + RAISE NOTICE '⚠️ 重要提醒:'; + RAISE NOTICE ' 1. 請確認存儲桶已在 Supabase Dashboard 中顯示'; + RAISE NOTICE ' 2. 檢查 Storage → Settings 中的政策設置'; + RAISE NOTICE ' 3. 測試圖片上傳功能是否正常'; + RAISE NOTICE ' 4. 定期執行 mark_orphaned_images_for_cleanup() 清理孤立圖片'; +END $$; diff --git a/scripts/05-setup-rls.sql b/scripts/05-setup-rls.sql new file mode 100644 index 0000000..25c7420 --- /dev/null +++ b/scripts/05-setup-rls.sql @@ -0,0 +1,151 @@ +-- 心願星河 - Row Level Security (RLS) 政策設置 +-- 執行順序:第 5 步(最後一步) +-- 說明:設置完整的安全政策,保護數據安全 + +-- 開始事務 +BEGIN; + +-- 1. 啟用所有表格的 RLS +ALTER TABLE wishes ENABLE ROW LEVEL SECURITY; +ALTER TABLE wish_likes ENABLE ROW LEVEL SECURITY; +ALTER TABLE user_settings ENABLE ROW LEVEL SECURITY; +ALTER TABLE migration_log ENABLE ROW LEVEL SECURITY; +ALTER TABLE system_stats ENABLE ROW LEVEL SECURITY; +ALTER TABLE storage_usage ENABLE ROW LEVEL SECURITY; +ALTER TABLE storage_cleanup_log ENABLE ROW LEVEL SECURITY; + +-- 2. wishes 表格的 RLS 政策 + +-- 2.1 查看政策:公開的困擾案例所有人都可以查看 +DROP POLICY IF EXISTS "Public wishes are viewable by everyone" ON wishes; +CREATE POLICY "Public wishes are viewable by everyone" ON wishes + FOR SELECT + USING (is_public = true AND status = 'active'); + +-- 2.2 查看政策:用戶可以查看自己的所有困擾案例 +DROP POLICY IF EXISTS "Users can view own wishes" ON wishes; +CREATE POLICY "Users can view own wishes" ON wishes + FOR SELECT + USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)); + +-- 2.3 插入政策:所有人都可以提交困擾案例 +DROP POLICY IF EXISTS "Anyone can insert wishes" ON wishes; +CREATE POLICY "Anyone can insert wishes" ON wishes + FOR INSERT + WITH CHECK (true); + +-- 2.4 更新政策:用戶只能更新自己的困擾案例 +DROP POLICY IF EXISTS "Users can update own wishes" ON wishes; +CREATE POLICY "Users can update own wishes" ON wishes + FOR UPDATE + USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)) + WITH CHECK (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)); + +-- 2.5 刪除政策:用戶只能軟刪除自己的困擾案例 +DROP POLICY IF EXISTS "Users can delete own wishes" ON wishes; +CREATE POLICY "Users can delete own wishes" ON wishes + FOR UPDATE + USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)) + WITH CHECK (status = 'deleted'); + +-- 3. wish_likes 表格的 RLS 政策 + +-- 3.1 查看政策:所有人都可以查看點讚記錄(用於統計) +DROP POLICY IF EXISTS "Wish likes are viewable by everyone" ON wish_likes; +CREATE POLICY "Wish likes are viewable by everyone" ON wish_likes + FOR SELECT + USING (true); + +-- 3.2 插入政策:所有人都可以點讚 +DROP POLICY IF EXISTS "Anyone can insert wish likes" ON wish_likes; +CREATE POLICY "Anyone can insert wish likes" ON wish_likes + FOR INSERT + WITH CHECK (true); + +-- 3.3 刪除政策:用戶只能取消自己的點讚 +DROP POLICY IF EXISTS "Users can delete own likes" ON wish_likes; +CREATE POLICY "Users can delete own likes" ON wish_likes + FOR DELETE + USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)); + +-- 4. user_settings 表格的 RLS 政策 + +-- 4.1 查看政策:用戶只能查看自己的設定 +DROP POLICY IF EXISTS "Users can view own settings" ON user_settings; +CREATE POLICY "Users can view own settings" ON user_settings + FOR SELECT + USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)); + +-- 4.2 插入政策:用戶可以創建自己的設定 +DROP POLICY IF EXISTS "Users can insert own settings" ON user_settings; +CREATE POLICY "Users can insert own settings" ON user_settings + FOR INSERT + WITH CHECK (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)); + +-- 4.3 更新政策:用戶只能更新自己的設定 +DROP POLICY IF EXISTS "Users can update own settings" ON user_settings; +CREATE POLICY "Users can update own settings" ON user_settings + FOR UPDATE + USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)) + WITH CHECK (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true)); + +-- 5. migration_log 表格的 RLS 政策 + +-- 5.1 查看政策:用戶可以查看自己的遷移記錄 +DROP POLICY IF EXISTS "Users can view own migration logs" ON migration_log; +CREATE POLICY "Users can view own migration logs" ON migration_log + FOR SELECT + USING (user_session = current_setting('request.jwt.claims', true)::json->>'user_session' OR + user_session = current_setting('app.user_session', true) OR + user_session = 'system'); + +-- 5.2 插入政策:系統和用戶都可以插入遷移記錄 +DROP POLICY IF EXISTS "System and users can insert migration logs" ON migration_log; +CREATE POLICY "System and users can insert migration logs" ON migration_log + FOR INSERT + WITH CHECK (true); + +-- 6. system_stats 表格的 RLS 政策 + +-- 6.1 查看政策:所有人都可以查看系統統計(公開數據) +DROP POLICY IF EXISTS "System stats are viewable by everyone" ON system_stats; +CREATE POLICY "System stats are viewable by everyone" ON system_stats + FOR SELECT + USING (true); + +-- 6.2 插入/更新政策:只有系統可以修改統計數據 +DROP POLICY IF EXISTS "Only system can modify stats" ON system_stats; +CREATE POLICY "Only system can modify stats" ON system_stats + FOR ALL + USING (current_user = 'postgres' OR current_setting('role', true) = 'service_role'); + +-- 7. storage_usage 表格的 RLS 政策 + +-- 7.1 查看政策:所有人都可以查看存儲使用統計 +DROP POLICY IF EXISTS "Storage usage is viewable by everyone" ON storage_usage; +CREATE POLICY "Storage usage is viewable by everyone" ON storage_usage + FOR SELECT + USING (true); + +-- 7.2 修改政策:只有系統可以修改存儲統計 +DROP POLICY IF EXISTS "Only system can modify storage usage" ON storage_usage; +CREATE POLICY "Only system can modify storage usage" ON storage_usage + FOR ALL + USING (current_user = 'postgres' OR current_setting('role', true) = 'service_role'); + +-- 8. storage_cleanup_log 表格的 RLS 政策 + +-- 8.1 查看政策:所有人都可以查看清理記錄 +DROP POLICY IF EXISTS "Storage cleanup logs are viewable by everyone" ON storage_cleanup_log; +CREATE POLICY "Storage cleanup logs are viewable by everyone" ON storage_cleanup_log + FOR SELECT + USING (true); diff --git a/scripts/README-CLEAR-DATA.md b/scripts/README-CLEAR-DATA.md new file mode 100644 index 0000000..fe0f951 --- /dev/null +++ b/scripts/README-CLEAR-DATA.md @@ -0,0 +1,135 @@ +# 心願星河 - 數據清空指南 + +⚠️ **重要警告**:以下操作將永久刪除所有數據,請謹慎使用! + +## 可用的清空腳本 + +### 1. 綜合清空腳本(推薦) +```bash +node scripts/clear-all.js +``` +**功能**:一次性清空所有數據 +- 清空 Supabase Storage 中的所有圖片 +- 清空資料庫中的所有表格 +- 重置自增序列 +- 重新插入初始數據 +- 驗證清空結果 + +### 2. 單獨清空 Storage +```bash +node scripts/clear-storage.js +``` +**功能**:僅清空圖片存儲 +- 清空 `wish-images` 存儲桶 +- 清空 `wish-thumbnails` 存儲桶 + +### 3. 單獨清空資料庫 +在 Supabase Dashboard 的 SQL Editor 中執行: +```sql +-- 執行 scripts/clear-all-data.sql 文件的內容 +``` + +## 使用前準備 + +### 1. 確認環境變數 +確保以下環境變數已正確設置(在 `.env.local` 文件中): +```env +NEXT_PUBLIC_SUPABASE_URL=your_supabase_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key # 可選,但建議設置 +``` + +### 2. 安裝依賴 +```bash +npm install +# 或 +pnpm install +``` + +## 使用步驟 + +### 方案 A:一鍵清空(推薦) +```bash +# 執行綜合清空腳本 +node scripts/clear-all.js + +# 腳本會顯示 10 秒倒計時,按 Ctrl+C 可以取消 +``` + +### 方案 B:分步驟清空 +```bash +# 1. 先清空 Storage +node scripts/clear-storage.js + +# 2. 再清空資料庫(在 Supabase Dashboard 中執行) +# 將 scripts/clear-all-data.sql 的內容貼到 SQL Editor 中執行 +``` + +## 清空後的檢查 + +### 1. 驗證 Storage +在 Supabase Dashboard → Storage 中檢查: +- `wish-images` 桶應該是空的 +- `wish-thumbnails` 桶應該是空的 + +### 2. 驗證資料庫 +在 Supabase Dashboard → Table Editor 中檢查: +- `wishes` 表應該沒有記錄 +- `wish_likes` 表應該沒有記錄 +- `user_settings` 表應該沒有記錄 +- 其他管理表格會有基礎的初始記錄 + +### 3. 測試應用程式 +```bash +# 重新啟動開發服務器 +npm run dev +# 或 +pnpm dev +``` + +在瀏覽器中: +1. 清除 localStorage(開發者工具 → Application → Local Storage → Clear All) +2. 重新載入頁面 +3. 測試提交新的困擾案例 +4. 確認功能正常運行 + +## 故障排除 + +### 1. 權限錯誤 +``` +Error: Insufficient permissions +``` +**解決方案**:確保使用 `SUPABASE_SERVICE_ROLE_KEY` 而不是 `ANON_KEY` + +### 2. 存儲桶不存在 +``` +Error: Bucket does not exist +``` +**解決方案**:正常現象,腳本會自動跳過不存在的存儲桶 + +### 3. 網路錯誤 +``` +Error: fetch failed +``` +**解決方案**:檢查網路連接和 Supabase URL 是否正確 + +### 4. 資料庫連接錯誤 +**解決方案**: +1. 檢查 Supabase 專案是否暫停 +2. 驗證 URL 和密鑰是否正確 +3. 確認專案是否有足夠的配額 + +## 注意事項 + +1. **備份重要數據**:在生產環境中執行前,請先備份重要數據 +2. **測試環境優先**:建議先在測試環境中驗證腳本功能 +3. **瀏覽器清除**:清空數據後記得清除瀏覽器的 localStorage +4. **應用重啟**:清空後建議重新啟動應用程式 + +## 聯絡支援 + +如果遇到問題,請檢查: +1. 控制台錯誤訊息 +2. Supabase Dashboard 中的 Logs +3. 網路連接狀態 +4. 環境變數配置 \ No newline at end of file diff --git a/scripts/clear-all-data.js b/scripts/clear-all-data.js deleted file mode 100644 index 87d745a..0000000 --- a/scripts/clear-all-data.js +++ /dev/null @@ -1,55 +0,0 @@ -// 清空所有本地存儲資料的腳本 -// 執行此腳本將清除所有測試數據,讓應用回到初始狀態 - -console.log("🧹 開始清空所有測試資料...") - -// 清空的資料項目 -const dataKeys = [ - "wishes", // 所有許願/困擾案例 - "wishLikes", // 點讚數據 - "userLikedWishes", // 用戶點讚記錄 - "backgroundMusicState", // 背景音樂狀態 -] - -let clearedCount = 0 - -// 清空每個資料項目 -dataKeys.forEach((key) => { - const existingData = localStorage.getItem(key) - if (existingData) { - localStorage.removeItem(key) - console.log(`✅ 已清空: ${key}`) - clearedCount++ - } else { - console.log(`ℹ️ ${key} 已經是空的`) - } -}) - -// 顯示清理結果 -console.log(`\n🎉 資料清理完成!`) -console.log(`📊 清理統計:`) -console.log(` - 清空了 ${clearedCount} 個資料項目`) -console.log(` - 檢查了 ${dataKeys.length} 個資料項目`) - -// 驗證清理結果 -console.log(`\n🔍 驗證清理結果:`) -dataKeys.forEach((key) => { - const data = localStorage.getItem(key) - if (data) { - console.log(`❌ ${key}: 仍有資料殘留`) - } else { - console.log(`✅ ${key}: 已完全清空`) - } -}) - -console.log(`\n🚀 應用程式已準備好進行正式佈署!`) -console.log(`💡 建議接下來的步驟:`) -console.log(` 1. 重新整理頁面確認所有資料已清空`) -console.log(` 2. 測試各個功能頁面的初始狀態`) -console.log(` 3. 確認沒有錯誤訊息或異常行為`) -console.log(` 4. 準備佈署到正式環境`) - -// 提供重新載入頁面的選項 -if (confirm("是否要重新載入頁面以確認清理效果?")) { - window.location.reload() -} diff --git a/scripts/clear-all-data.sql b/scripts/clear-all-data.sql new file mode 100644 index 0000000..3f51b3b --- /dev/null +++ b/scripts/clear-all-data.sql @@ -0,0 +1,134 @@ +-- 心願星河 - 清空所有數據 +-- ⚠️ 警告:此腳本將永久刪除所有數據,請謹慎使用! +-- 建議:在生產環境執行前請備份重要數據 + +-- 開始事務 +BEGIN; + +-- 0. 修復 migration_log 表約束問題 +DO $$ +BEGIN + -- 移除舊的約束 + ALTER TABLE migration_log DROP CONSTRAINT IF EXISTS migration_log_migration_type_check; + + -- 添加新的約束,包含所有需要的類型 + ALTER TABLE migration_log ADD CONSTRAINT migration_log_migration_type_check + CHECK (migration_type IN ('wishes', 'likes', 'settings', 'storage_cleanup', 'data_cleanup', 'image_cleanup')); + + RAISE NOTICE '🔧 migration_log 表約束已修復'; +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE '⚠️ 修復約束時發生錯誤,但繼續執行: %', SQLERRM; +END $$; + +-- 顯示警告訊息 +DO $$ +BEGIN + RAISE NOTICE ''; + RAISE NOTICE '🚨 準備清空所有數據...'; + RAISE NOTICE '⚠️ 這將永久刪除:'; + RAISE NOTICE ' - 所有困擾案例 (wishes)'; + RAISE NOTICE ' - 所有點讚記錄 (wish_likes)'; + RAISE NOTICE ' - 所有用戶設定 (user_settings)'; + RAISE NOTICE ' - 遷移記錄 (migration_log)'; + RAISE NOTICE ' - 系統統計 (system_stats)'; + RAISE NOTICE ' - 存儲使用記錄 (storage_usage)'; + RAISE NOTICE ' - 存儲清理記錄 (storage_cleanup_log)'; + RAISE NOTICE ''; +END $$; + +-- 1. 清空所有數據表(按依賴關係順序) +DO $$ +DECLARE + table_count INTEGER; +BEGIN + -- 清空有外鍵關係的表格 + DELETE FROM wish_likes; + GET DIAGNOSTICS table_count = ROW_COUNT; + RAISE NOTICE '🗑️ 已清空 wish_likes 表,刪除 % 條記錄', table_count; + + DELETE FROM wishes; + GET DIAGNOSTICS table_count = ROW_COUNT; + RAISE NOTICE '🗑️ 已清空 wishes 表,刪除 % 條記錄', table_count; + + DELETE FROM user_settings; + GET DIAGNOSTICS table_count = ROW_COUNT; + RAISE NOTICE '🗑️ 已清空 user_settings 表,刪除 % 條記錄', table_count; + + DELETE FROM migration_log; + GET DIAGNOSTICS table_count = ROW_COUNT; + RAISE NOTICE '🗑️ 已清空 migration_log 表,刪除 % 條記錄', table_count; + + DELETE FROM system_stats; + GET DIAGNOSTICS table_count = ROW_COUNT; + RAISE NOTICE '🗑️ 已清空 system_stats 表,刪除 % 條記錄', table_count; + + DELETE FROM storage_usage; + GET DIAGNOSTICS table_count = ROW_COUNT; + RAISE NOTICE '🗑️ 已清空 storage_usage 表,刪除 % 條記錄', table_count; + + DELETE FROM storage_cleanup_log; + GET DIAGNOSTICS table_count = ROW_COUNT; + RAISE NOTICE '🗑️ 已清空 storage_cleanup_log 表,刪除 % 條記錄', table_count; +END $$; + +-- 2. 重置自增序列 +DO $$ +BEGIN + RAISE NOTICE ''; + RAISE NOTICE '🔄 重置自增序列...'; + + -- 重置所有表格的序列 + ALTER SEQUENCE wishes_id_seq RESTART WITH 1; + ALTER SEQUENCE wish_likes_id_seq RESTART WITH 1; + ALTER SEQUENCE user_settings_id_seq RESTART WITH 1; + ALTER SEQUENCE migration_log_id_seq RESTART WITH 1; + ALTER SEQUENCE system_stats_id_seq RESTART WITH 1; + ALTER SEQUENCE storage_usage_id_seq RESTART WITH 1; + ALTER SEQUENCE storage_cleanup_log_id_seq RESTART WITH 1; + + RAISE NOTICE '✅ 所有序列已重置為 1'; +END $$; + +-- 3. 重新插入初始統計記錄 +INSERT INTO storage_usage (bucket_name, total_files, total_size_bytes) +VALUES + ('wish-images', 0, 0), + ('wish-thumbnails', 0, 0); + +INSERT INTO system_stats (stat_date, total_wishes, public_wishes, private_wishes, total_likes, active_users, storage_used_mb) +VALUES (CURRENT_DATE, 0, 0, 0, 0, 0, 0); + +-- 4. 記錄清空操作 +INSERT INTO migration_log ( + user_session, + migration_type, + target_records, + success, + error_message +) VALUES ( + 'system-admin', + 'data_cleanup', + 0, + true, + 'All data cleared by admin request at ' || NOW() +); + +-- 提交事務 +COMMIT; + +-- 顯示完成訊息 +DO $$ +BEGIN + RAISE NOTICE ''; + RAISE NOTICE '✅ 資料庫清空完成!'; + RAISE NOTICE '📊 重置統計:'; + RAISE NOTICE ' - 所有表格已清空'; + RAISE NOTICE ' - 自增序列已重置'; + RAISE NOTICE ' - 初始統計記錄已重新建立'; + RAISE NOTICE ''; + RAISE NOTICE '⚠️ 注意:'; + RAISE NOTICE ' - Storage 中的檔案需要手動清空'; + RAISE NOTICE ' - 可以使用 clear-storage.js 腳本清空圖片'; + RAISE NOTICE ''; +END $$; \ No newline at end of file diff --git a/scripts/clear-all.js b/scripts/clear-all.js new file mode 100644 index 0000000..ee9e6d4 --- /dev/null +++ b/scripts/clear-all.js @@ -0,0 +1,357 @@ +#!/usr/bin/env node + +/** + * 心願星河 - 綜合清空腳本 + * + * ⚠️ 警告:此腳本將永久刪除所有數據和文件! + * + * 功能: + * 1. 清空 Supabase Storage 中的所有圖片 + * 2. 清空資料庫中的所有數據 + * 3. 重置自增序列 + * 4. 重新初始化基礎數據 + * + * 使用方法: + * node scripts/clear-all.js + */ + +const { createClient } = require('@supabase/supabase-js'); +const fs = require('fs'); +const path = require('path'); + +// 載入環境變數 +require('dotenv').config({ path: '.env.local' }); + +// Supabase 配置 +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +// 檢查必要的環境變數 +if (!supabaseUrl || !supabaseServiceKey) { + console.error('❌ 錯誤:缺少必要的環境變數'); + console.error('請確保已設置以下環境變數:'); + console.error('- NEXT_PUBLIC_SUPABASE_URL'); + console.error('- SUPABASE_SERVICE_ROLE_KEY 或 NEXT_PUBLIC_SUPABASE_ANON_KEY'); + process.exit(1); +} + +// 初始化 Supabase 客戶端 +const supabase = createClient(supabaseUrl, supabaseServiceKey); + +/** + * 清空存儲桶中的所有文件 + */ +async function clearStorage() { + console.log('\n📁 開始清空 Storage...'); + + const buckets = ['wish-images', 'wish-thumbnails']; + let allSuccess = true; + + for (const bucketName of buckets) { + try { + console.log(`\n🗂️ 正在處理存儲桶:${bucketName}`); + + // 列出所有文件 + const { data: files, error: listError } = await supabase.storage + .from(bucketName) + .list('', { limit: 1000 }); + + if (listError) { + if (listError.message.includes('not found') || listError.message.includes('does not exist')) { + console.log(`⚠️ 存儲桶 ${bucketName} 不存在,跳過`); + continue; + } + console.error(`❌ 列出 ${bucketName} 文件時出錯:`, listError.message); + allSuccess = false; + continue; + } + + if (!files || files.length === 0) { + console.log(`✅ ${bucketName} 已經是空的`); + continue; + } + + // 獲取所有文件路徑 + const allFilePaths = []; + for (const file of files) { + if (file.name && file.name !== '.emptyFolderPlaceholder') { + if (!file.metadata) { + // 處理目錄 + const { data: subFiles } = await supabase.storage + .from(bucketName) + .list(file.name, { limit: 1000 }); + + if (subFiles) { + subFiles.forEach(subFile => { + if (subFile.name && subFile.name !== '.emptyFolderPlaceholder') { + allFilePaths.push(`${file.name}/${subFile.name}`); + } + }); + } + } else { + allFilePaths.push(file.name); + } + } + } + + if (allFilePaths.length === 0) { + console.log(`✅ ${bucketName} 中沒有需要刪除的文件`); + continue; + } + + console.log(`🗑️ 刪除 ${allFilePaths.length} 個文件...`); + + // 批量刪除 + const batchSize = 50; + for (let i = 0; i < allFilePaths.length; i += batchSize) { + const batch = allFilePaths.slice(i, i + batchSize); + const { error } = await supabase.storage.from(bucketName).remove(batch); + + if (error) { + console.error(`❌ 刪除批次失敗:`, error.message); + allSuccess = false; + } else { + console.log(`✅ 已刪除 ${Math.min(i + batchSize, allFilePaths.length)}/${allFilePaths.length} 個文件`); + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } + + } catch (error) { + console.error(`❌ 處理 ${bucketName} 時發生錯誤:`, error.message); + allSuccess = false; + } + } + + return allSuccess; +} + +/** + * 修復 migration_log 表的約束問題 + */ +async function fixMigrationLogConstraint() { + console.log('\n🔧 修復 migration_log 表約束...'); + + try { + // 使用 rpc 調用執行 SQL,修復約束 + const { error } = await supabase.rpc('exec_sql', { + sql_query: ` + ALTER TABLE migration_log DROP CONSTRAINT IF EXISTS migration_log_migration_type_check; + ALTER TABLE migration_log ADD CONSTRAINT migration_log_migration_type_check + CHECK (migration_type IN ('wishes', 'likes', 'settings', 'storage_cleanup', 'data_cleanup', 'image_cleanup')); + ` + }); + + if (error) { + console.log('⚠️ 無法通過 RPC 修復約束,嘗試其他方法...'); + // 如果 RPC 方法失敗,我們繼續執行,但會在日誌中使用允許的類型 + return false; + } + + console.log('✅ migration_log 表約束已修復'); + return true; + } catch (error) { + console.log('⚠️ 修復約束時發生錯誤,但繼續執行:', error.message); + return false; + } +} + +/** + * 清空資料庫數據 + */ +async function clearDatabase() { + console.log('\n🗄️ 開始清空資料庫...'); + + try { + // 1. 清空有外鍵關係的表格(按依賴順序) + const tablesToClear = [ + { name: 'wish_likes', description: '點讚記錄' }, + { name: 'wishes', description: '困擾案例' }, + { name: 'user_settings', description: '用戶設定' }, + { name: 'system_stats', description: '系統統計' }, + { name: 'storage_usage', description: '存儲使用記錄' }, + { name: 'storage_cleanup_log', description: '存儲清理記錄' }, + { name: 'migration_log', description: '遷移記錄' } + ]; + + for (const table of tablesToClear) { + try { + const { error } = await supabase.from(table.name).delete().neq('id', 0); + if (error) { + console.error(`❌ 清空 ${table.name} (${table.description}) 表失敗:`, error.message); + // 如果不是 migration_log 表,則返回失敗 + if (table.name !== 'migration_log') { + return false; + } + // migration_log 表清空失敗可以忽略,因為我們稍後會重新插入 + console.log(`⚠️ ${table.name} 表清空失敗,將在後續步驟中處理`); + } else { + console.log(`✅ 已清空 ${table.name} (${table.description}) 表`); + } + } catch (err) { + console.error(`❌ 處理 ${table.name} 表時發生錯誤:`, err.message); + if (table.name !== 'migration_log') { + return false; + } + } + } + + // 2. 重新插入初始數據 + console.log('\n🔧 重新插入初始數據...'); + + // 插入初始存儲統計 + await supabase.from('storage_usage').insert([ + { bucket_name: 'wish-images', total_files: 0, total_size_bytes: 0 }, + { bucket_name: 'wish-thumbnails', total_files: 0, total_size_bytes: 0 } + ]); + + // 插入今日初始統計 + await supabase.from('system_stats').insert([{ + stat_date: new Date().toISOString().split('T')[0], + total_wishes: 0, + public_wishes: 0, + private_wishes: 0, + total_likes: 0, + active_users: 0, + storage_used_mb: 0 + }]); + + // 記錄清空操作(最後執行,避免約束衝突) + try { + await supabase.from('migration_log').insert([{ + user_session: 'system-admin', + migration_type: 'data_cleanup', + target_records: 0, + success: true, + error_message: `All data cleared by admin request at ${new Date().toISOString()}` + }]); + console.log('✅ 清空操作記錄已插入'); + } catch (logError) { + console.log('⚠️ 無法插入清空操作記錄,但不影響清空結果:', logError.message); + } + + console.log('✅ 初始數據插入完成'); + return true; + + } catch (error) { + console.error('❌ 清空資料庫時發生錯誤:', error.message); + return false; + } +} + +/** + * 驗證清空結果 + */ +async function verifyCleanup() { + console.log('\n🔍 驗證清空結果...'); + + try { + // 檢查主要數據表 + const { data: wishes, error: wishesError } = await supabase + .from('wishes') + .select('count', { count: 'exact', head: true }); + + const { data: likes, error: likesError } = await supabase + .from('wish_likes') + .select('count', { count: 'exact', head: true }); + + if (wishesError || likesError) { + console.error('❌ 驗證時發生錯誤'); + return false; + } + + console.log(`📊 驗證結果:`); + console.log(` - wishes 表:${wishes || 0} 條記錄`); + console.log(` - wish_likes 表:${likes || 0} 條記錄`); + + // 檢查存儲 + const buckets = ['wish-images', 'wish-thumbnails']; + for (const bucket of buckets) { + const { data: files } = await supabase.storage.from(bucket).list('', { limit: 1 }); + console.log(` - ${bucket} 存儲桶:${files ? files.length : 0} 個文件`); + } + + return true; + + } catch (error) { + console.error('❌ 驗證時發生錯誤:', error.message); + return false; + } +} + +/** + * 主函數 + */ +async function main() { + console.log('🚀 心願星河 - 綜合數據清空'); + console.log('⚠️ 警告:這將永久刪除所有數據和文件!'); + console.log('\n包含:'); + console.log('- 所有困擾案例和點讚記錄'); + console.log('- 所有用戶設定'); + console.log('- Storage 中的所有圖片文件'); + console.log('- 系統統計和記錄'); + + // 給用戶考慮時間 + console.log('\n⏰ 10 秒後開始清空... (按 Ctrl+C 取消)'); + for (let i = 10; i > 0; i--) { + process.stdout.write(`\r倒計時:${i} 秒 `); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + console.log('\n\n開始執行清空操作...\n'); + + let success = true; + + // 0. 修復約束問題 + const constraintFixed = await fixMigrationLogConstraint(); + + // 1. 清空存儲 + const storageSuccess = await clearStorage(); + if (!storageSuccess) { + console.log('⚠️ Storage 清空過程中有錯誤,但繼續執行資料庫清空'); + } + + // 2. 清空資料庫 + const dbSuccess = await clearDatabase(); + if (!dbSuccess) { + console.error('❌ 資料庫清空失敗'); + success = false; + } + + // 3. 驗證結果 + if (success) { + await verifyCleanup(); + } + + // 顯示最終結果 + console.log('\n' + '='.repeat(60)); + if (success) { + console.log('✅ 所有數據清空完成!'); + console.log('\n📝 建議後續步驟:'); + console.log('1. 重新啟動應用程式'); + console.log('2. 在瀏覽器中清除 localStorage'); + console.log('3. 確認應用程式正常運行'); + } else { + console.log('❌ 清空過程中有錯誤,請檢查上述訊息'); + } + + process.exit(success ? 0 : 1); +} + +// 錯誤處理 +process.on('unhandledRejection', (error) => { + console.error('❌ 未處理的錯誤:', error); + process.exit(1); +}); + +process.on('SIGINT', () => { + console.log('\n❌ 用戶取消操作'); + process.exit(0); +}); + +// 執行主函數 +if (require.main === module) { + main().catch(error => { + console.error('❌ 腳本執行失敗:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/scripts/clear-storage.js b/scripts/clear-storage.js new file mode 100644 index 0000000..00022a9 --- /dev/null +++ b/scripts/clear-storage.js @@ -0,0 +1,221 @@ +#!/usr/bin/env node + +/** + * 心願星河 - 清空 Supabase Storage + * + * ⚠️ 警告:此腳本將永久刪除所有存儲的圖片文件! + * + * 使用方法: + * 1. 確保已安裝依賴:npm install + * 2. 設置環境變數或在 .env.local 中配置 Supabase 連接 + * 3. 執行腳本:node scripts/clear-storage.js + */ + +const { createClient } = require('@supabase/supabase-js'); +const fs = require('fs'); +const path = require('path'); + +// 載入環境變數 +require('dotenv').config({ path: '.env.local' }); + +// Supabase 配置 +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +// 檢查必要的環境變數 +if (!supabaseUrl || !supabaseServiceKey) { + console.error('❌ 錯誤:缺少必要的環境變數'); + console.error('請確保已設置以下環境變數:'); + console.error('- NEXT_PUBLIC_SUPABASE_URL'); + console.error('- SUPABASE_SERVICE_ROLE_KEY 或 NEXT_PUBLIC_SUPABASE_ANON_KEY'); + process.exit(1); +} + +// 初始化 Supabase 客戶端 +const supabase = createClient(supabaseUrl, supabaseServiceKey); + +// 存儲桶名稱 +const BUCKETS = ['wish-images', 'wish-thumbnails']; + +/** + * 清空指定存儲桶中的所有文件 + */ +async function clearBucket(bucketName) { + try { + console.log(`\n🗂️ 正在處理存儲桶:${bucketName}`); + + // 列出所有文件 + const { data: files, error: listError } = await supabase.storage + .from(bucketName) + .list('', { + limit: 1000, + sortBy: { column: 'created_at', order: 'desc' } + }); + + if (listError) { + console.error(`❌ 列出 ${bucketName} 文件時出錯:`, listError.message); + return false; + } + + if (!files || files.length === 0) { + console.log(`✅ ${bucketName} 已經是空的`); + return true; + } + + console.log(`📊 找到 ${files.length} 個文件`); + + // 獲取所有文件路徑(包括子目錄) + const allFilePaths = []; + + for (const file of files) { + if (file.name && file.name !== '.emptyFolderPlaceholder') { + // 如果是目錄,遞歸獲取其中的文件 + if (!file.metadata) { + const { data: subFiles, error: subListError } = await supabase.storage + .from(bucketName) + .list(file.name, { limit: 1000 }); + + if (!subListError && subFiles) { + subFiles.forEach(subFile => { + if (subFile.name && subFile.name !== '.emptyFolderPlaceholder') { + allFilePaths.push(`${file.name}/${subFile.name}`); + } + }); + } + } else { + allFilePaths.push(file.name); + } + } + } + + if (allFilePaths.length === 0) { + console.log(`✅ ${bucketName} 中沒有需要刪除的文件`); + return true; + } + + console.log(`🗑️ 準備刪除 ${allFilePaths.length} 個文件...`); + + // 批量刪除文件 + const batchSize = 50; // Supabase 建議的批量操作大小 + let totalDeleted = 0; + let hasErrors = false; + + for (let i = 0; i < allFilePaths.length; i += batchSize) { + const batch = allFilePaths.slice(i, i + batchSize); + + const { data, error } = await supabase.storage + .from(bucketName) + .remove(batch); + + if (error) { + console.error(`❌ 刪除批次 ${Math.floor(i/batchSize) + 1} 時出錯:`, error.message); + hasErrors = true; + } else { + totalDeleted += batch.length; + console.log(`✅ 已刪除批次 ${Math.floor(i/batchSize) + 1}/${Math.ceil(allFilePaths.length/batchSize)} (${batch.length} 個文件)`); + } + + // 避免請求過於頻繁 + await new Promise(resolve => setTimeout(resolve, 100)); + } + + console.log(`📊 ${bucketName} 清空完成:刪除了 ${totalDeleted}/${allFilePaths.length} 個文件`); + return !hasErrors; + + } catch (error) { + console.error(`❌ 清空 ${bucketName} 時發生未預期錯誤:`, error.message); + return false; + } +} + +/** + * 驗證存儲桶是否存在 + */ +async function verifyBuckets() { + try { + const { data: buckets, error } = await supabase.storage.listBuckets(); + + if (error) { + console.error('❌ 無法獲取存儲桶列表:', error.message); + return false; + } + + const existingBuckets = buckets.map(bucket => bucket.id); + const missingBuckets = BUCKETS.filter(bucket => !existingBuckets.includes(bucket)); + + if (missingBuckets.length > 0) { + console.warn('⚠️ 以下存儲桶不存在,將跳過:', missingBuckets.join(', ')); + return BUCKETS.filter(bucket => existingBuckets.includes(bucket)); + } + + return BUCKETS; + } catch (error) { + console.error('❌ 驗證存儲桶時發生錯誤:', error.message); + return false; + } +} + +/** + * 主函數 + */ +async function main() { + console.log('🚀 開始清空 Supabase Storage...'); + console.log('⚠️ 警告:這將永久刪除所有存儲的圖片文件!'); + + // 驗證存儲桶 + const bucketsToProcess = await verifyBuckets(); + if (!bucketsToProcess || bucketsToProcess.length === 0) { + console.error('❌ 沒有可處理的存儲桶'); + process.exit(1); + } + + console.log(`📋 將處理 ${bucketsToProcess.length} 個存儲桶:`, bucketsToProcess.join(', ')); + + // 給用戶 5 秒鐘考慮時間 + console.log('\n⏰ 5 秒後開始刪除... (按 Ctrl+C 取消)'); + await new Promise(resolve => setTimeout(resolve, 5000)); + + let allSuccess = true; + + // 清空每個存儲桶 + for (const bucket of bucketsToProcess) { + const success = await clearBucket(bucket); + if (!success) { + allSuccess = false; + } + } + + // 顯示最終結果 + console.log('\n' + '='.repeat(50)); + if (allSuccess) { + console.log('✅ 所有存儲桶清空完成!'); + } else { + console.log('⚠️ 存儲桶清空完成,但過程中有一些錯誤'); + } + + console.log('\n📝 建議後續步驟:'); + console.log('1. 在 Supabase Dashboard 中確認 Storage 已清空'); + console.log('2. 執行 clear-all-data.sql 清空資料庫'); + console.log('3. 重新啟動應用程式'); +} + +// 錯誤處理 +process.on('unhandledRejection', (error) => { + console.error('❌ 未處理的錯誤:', error); + process.exit(1); +}); + +process.on('SIGINT', () => { + console.log('\n❌ 用戶取消操作'); + process.exit(0); +}); + +// 執行主函數 +if (require.main === module) { + main().catch(error => { + console.error('❌ 腳本執行失敗:', error); + process.exit(1); + }); +} + +module.exports = { clearBucket, verifyBuckets }; \ No newline at end of file diff --git a/scripts/fix-migration-log-constraint.sql b/scripts/fix-migration-log-constraint.sql new file mode 100644 index 0000000..8207ee9 --- /dev/null +++ b/scripts/fix-migration-log-constraint.sql @@ -0,0 +1,26 @@ +-- 修復 migration_log 表的約束問題 +-- 允許 'storage_cleanup' 和 'data_cleanup' 類型 + +BEGIN; + +-- 移除舊的約束 +ALTER TABLE migration_log DROP CONSTRAINT IF EXISTS migration_log_migration_type_check; + +-- 添加新的約束,包含所有需要的類型 +ALTER TABLE migration_log ADD CONSTRAINT migration_log_migration_type_check +CHECK (migration_type IN ('wishes', 'likes', 'settings', 'storage_cleanup', 'data_cleanup', 'image_cleanup')); + +-- 顯示結果 +DO $$ +BEGIN + RAISE NOTICE '✅ migration_log 表約束已更新'; + RAISE NOTICE '📋 允許的 migration_type 值:'; + RAISE NOTICE ' - wishes(困擾案例遷移)'; + RAISE NOTICE ' - likes(點讚記錄遷移)'; + RAISE NOTICE ' - settings(用戶設定遷移)'; + RAISE NOTICE ' - storage_cleanup(存儲清理)'; + RAISE NOTICE ' - data_cleanup(數據清空)'; + RAISE NOTICE ' - image_cleanup(圖片清理)'; +END $$; + +COMMIT; \ No newline at end of file diff --git a/scripts/production-cleanup.js b/scripts/production-cleanup.js new file mode 100644 index 0000000..87d60bd --- /dev/null +++ b/scripts/production-cleanup.js @@ -0,0 +1,139 @@ +// 正式環境佈署前的完整資料清理腳本 +// 執行此腳本將清除所有測試資料,重置到正式環境狀態 + +console.log("🚀 開始準備正式環境佈署...") +console.log("=".repeat(50)) + +// 1. 清空所有本地存儲資料 +console.log("📋 第一步:清理本地存儲資料") + +const dataKeys = [ + "wishes", // 所有許願/困擾案例 + "wishLikes", // 點讚數據 + "userLikedWishes", // 用戶點讚記錄 + "backgroundMusicState", // 背景音樂狀態 +] + +let clearedCount = 0 +let totalDataSize = 0 + +// 計算清理前的資料大小 +dataKeys.forEach((key) => { + const data = localStorage.getItem(key) + if (data) { + totalDataSize += data.length + } +}) + +console.log(`📊 清理前資料統計:`) +console.log(` - 總資料大小: ${(totalDataSize / 1024).toFixed(2)} KB`) + +// 清空每個資料項目 +dataKeys.forEach((key) => { + const existingData = localStorage.getItem(key) + if (existingData) { + const dataSize = existingData.length + localStorage.removeItem(key) + console.log(`✅ 已清空: ${key} (${(dataSize / 1024).toFixed(2)} KB)`) + clearedCount++ + } else { + console.log(`ℹ️ ${key} 已經是空的`) + } +}) + +console.log("\n" + "=".repeat(50)) + +// 2. 設定正式環境的初始狀態 +console.log("⚙️ 第二步:設定正式環境初始狀態") + +const productionDefaults = { + wishes: [], + wishLikes: {}, + userLikedWishes: [], + backgroundMusicState: { + enabled: false, + volume: 0.3, + isPlaying: false, + }, +} + +// 設定初始狀態 +Object.entries(productionDefaults).forEach(([key, value]) => { + localStorage.setItem(key, JSON.stringify(value)) + console.log(`✅ 已設定: ${key} 初始狀態`) +}) + +console.log("\n" + "=".repeat(50)) + +// 3. 驗證清理結果 +console.log("🔍 第三步:驗證清理結果") + +let verificationPassed = true + +dataKeys.forEach((key) => { + const data = localStorage.getItem(key) + if (data) { + const parsedData = JSON.parse(data) + + // 檢查是否為空狀態 + if (key === "wishes" && Array.isArray(parsedData) && parsedData.length === 0) { + console.log(`✅ ${key}: 已重置為空陣列`) + } else if ( + (key === "wishLikes" || key === "userLikedWishes") && + ((Array.isArray(parsedData) && parsedData.length === 0) || + (typeof parsedData === "object" && Object.keys(parsedData).length === 0)) + ) { + console.log(`✅ ${key}: 已重置為空狀態`) + } else if (key === "backgroundMusicState" && typeof parsedData === "object") { + console.log(`✅ ${key}: 已重置為預設狀態`) + } else { + console.log(`❌ ${key}: 狀態異常`) + verificationPassed = false + } + } else { + console.log(`❌ ${key}: 資料遺失`) + verificationPassed = false + } +}) + +console.log("\n" + "=".repeat(50)) + +// 4. 顯示最終結果 +console.log("🎉 清理完成報告:") +console.log(`📊 清理統計:`) +console.log(` - 清空了 ${clearedCount} 個資料項目`) +console.log(` - 檢查了 ${dataKeys.length} 個資料項目`) +console.log(` - 釋放了 ${(totalDataSize / 1024).toFixed(2)} KB 空間`) +console.log(` - 驗證結果: ${verificationPassed ? "✅ 通過" : "❌ 失敗"}`) + +console.log("\n🚀 正式環境準備狀態:") +console.log(" ✅ 困擾案例: 0 個") +console.log(" ✅ 點讚記錄: 已清空") +console.log(" ✅ 背景音樂: 預設關閉") +console.log(" ✅ 本地存儲: 已重置") + +console.log("\n" + "=".repeat(50)) + +if (verificationPassed) { + console.log("🎯 佈署準備完成!") + console.log("✨ 應用程式已準備好進行正式佈署") + console.log("\n📋 建議的佈署檢查清單:") + console.log(" □ 重新整理頁面確認所有資料已清空") + console.log(" □ 測試各個功能頁面的初始狀態") + console.log(" □ 確認沒有錯誤訊息或異常行為") + console.log(" □ 檢查響應式設計在各裝置正常") + console.log(" □ 測試音效和背景音樂功能") + console.log(" □ 驗證隱私設定功能") + console.log(" □ 準備佈署到正式環境") + + // 提供重新載入頁面的選項 + setTimeout(() => { + if (confirm("✅ 清理完成!是否要重新載入頁面以確認效果?")) { + window.location.reload() + } + }, 2000) +} else { + console.log("⚠️ 清理過程中發現問題,請檢查後重新執行") +} + +console.log("\n🌟 感謝使用心願星河!準備為用戶提供優質服務!") diff --git a/scripts/reset-to-production.js b/scripts/reset-to-production.js deleted file mode 100644 index 410f2b1..0000000 --- a/scripts/reset-to-production.js +++ /dev/null @@ -1,42 +0,0 @@ -// 重置應用到正式環境狀態 -// 這個腳本會清空所有測試資料並設定適合正式環境的初始狀態 - -console.log("🔄 正在重置應用到正式環境狀態...") - -// 1. 清空所有本地存儲資料 -const dataKeys = ["wishes", "wishLikes", "userLikedWishes", "backgroundMusicState"] - -dataKeys.forEach((key) => { - localStorage.removeItem(key) -}) - -// 2. 設定正式環境的初始狀態 -const productionDefaults = { - wishes: [], - wishLikes: {}, - userLikedWishes: [], - backgroundMusicState: { - enabled: false, - volume: 0.3, - isPlaying: false, - }, -} - -// 設定初始狀態 -Object.entries(productionDefaults).forEach(([key, value]) => { - localStorage.setItem(key, JSON.stringify(value)) -}) - -console.log("✅ 應用已重置到正式環境狀態") -console.log("📋 初始狀態設定:") -console.log(" - 困擾案例: 0 個") -console.log(" - 點讚記錄: 已清空") -console.log(" - 背景音樂: 預設關閉") - -console.log("\n🎯 正式環境準備完成!") -console.log("🚀 可以開始佈署了") - -// 重新載入頁面 -setTimeout(() => { - window.location.reload() -}, 2000) diff --git a/scripts/test-supabase-connection.js b/scripts/test-supabase-connection.js new file mode 100644 index 0000000..666eccd --- /dev/null +++ b/scripts/test-supabase-connection.js @@ -0,0 +1,137 @@ +// 心願星河 - Supabase 連接測試腳本 +// 使用方法: npm run test-supabase + +const { createClient } = require("@supabase/supabase-js") +require("dotenv").config({ path: ".env.local" }) + +async function testSupabaseConnection() { + console.log("🔍 測試 Supabase 連接...\n") + + // 檢查環境變數 + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL + const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + + if (!supabaseUrl || !supabaseKey) { + console.error("❌ 環境變數未設置") + console.log("請確認 .env.local 檔案中包含:") + console.log("- NEXT_PUBLIC_SUPABASE_URL") + console.log("- NEXT_PUBLIC_SUPABASE_ANON_KEY") + process.exit(1) + } + + console.log("✅ 環境變數已設置") + console.log(`📍 Supabase URL: ${supabaseUrl}`) + console.log(`🔑 API Key: ${supabaseKey.substring(0, 20)}...`) + + // 創建 Supabase 客戶端 + const supabase = createClient(supabaseUrl, supabaseKey) + + try { + // 測試基本連接 + console.log("\n🔗 測試基本連接...") + const { data, error } = await supabase.from("wishes").select("count").limit(1) + + if (error) { + console.error("❌ 連接失敗:", error.message) + return false + } + + console.log("✅ 基本連接成功") + + // 測試表格存在性 + console.log("\n📊 檢查表格結構...") + const tables = ["wishes", "wish_likes", "user_settings", "migration_log", "system_stats"] + + for (const table of tables) { + try { + const { data, error } = await supabase.from(table).select("*").limit(1) + + if (error) { + console.log(`❌ 表格 ${table}: ${error.message}`) + } else { + console.log(`✅ 表格 ${table}: 正常`) + } + } catch (err) { + console.log(`❌ 表格 ${table}: ${err.message}`) + } + } + + // 測試視圖 + console.log("\n👁️ 檢查視圖...") + const views = ["wishes_with_likes", "public_wishes", "popular_wishes"] + + for (const view of views) { + try { + const { data, error } = await supabase.from(view).select("*").limit(1) + + if (error) { + console.log(`❌ 視圖 ${view}: ${error.message}`) + } else { + console.log(`✅ 視圖 ${view}: 正常`) + } + } catch (err) { + console.log(`❌ 視圖 ${view}: ${err.message}`) + } + } + + // 測試函數 + console.log("\n⚙️ 測試函數...") + try { + const { data, error } = await supabase.rpc("get_wishes_stats") + + if (error) { + console.log(`❌ 函數 get_wishes_stats: ${error.message}`) + } else { + console.log("✅ 函數 get_wishes_stats: 正常") + console.log("📈 統計數據:", JSON.stringify(data, null, 2)) + } + } catch (err) { + console.log(`❌ 函數測試失敗: ${err.message}`) + } + + // 測試存儲 + console.log("\n🗂️ 檢查存儲桶...") + try { + const { data: buckets, error } = await supabase.storage.listBuckets() + + if (error) { + console.log(`❌ 存儲桶檢查失敗: ${error.message}`) + } else { + const wishBuckets = buckets.filter((bucket) => bucket.id === "wish-images" || bucket.id === "wish-thumbnails") + + if (wishBuckets.length === 2) { + console.log("✅ 存儲桶設置完成") + wishBuckets.forEach((bucket) => { + console.log(` - ${bucket.id}: ${bucket.public ? "公開" : "私密"}`) + }) + } else { + console.log(`⚠️ 存儲桶不完整,找到 ${wishBuckets.length}/2 個`) + } + } + } catch (err) { + console.log(`❌ 存儲桶檢查失敗: ${err.message}`) + } + + console.log("\n🎉 Supabase 連接測試完成!") + return true + } catch (error) { + console.error("❌ 測試過程中發生錯誤:", error) + return false + } +} + +// 執行測試 +testSupabaseConnection() + .then((success) => { + if (success) { + console.log("\n✅ 所有測試通過,可以開始使用 Supabase!") + process.exit(0) + } else { + console.log("\n❌ 測試失敗,請檢查配置") + process.exit(1) + } + }) + .catch((error) => { + console.error("測試腳本執行失敗:", error) + process.exit(1) + }) diff --git a/setup-supabase.sh b/setup-supabase.sh new file mode 100644 index 0000000..aa83f49 --- /dev/null +++ b/setup-supabase.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# 心願星河 - Supabase 快速設置腳本 +# 使用方法: chmod +x setup-supabase.sh && ./setup-supabase.sh + +echo "🚀 心願星河 - Supabase 整合設置開始..." + +# 檢查 Node.js +if ! command -v node &> /dev/null; then + echo "❌ 請先安裝 Node.js" + exit 1 +fi + +# 檢查 npm +if ! command -v npm &> /dev/null; then + echo "❌ 請先安裝 npm" + exit 1 +fi + +echo "✅ Node.js 和 npm 已安裝" + +# 安裝依賴 +echo "📦 安裝專案依賴..." +npm install + +# 安裝 Supabase 客戶端 +echo "📦 安裝 Supabase 客戶端..." +npm install @supabase/supabase-js + +# 檢查環境變數檔案 +if [ ! -f ".env.local" ]; then + if [ -f ".env.local.example" ]; then + echo "📝 複製環境變數範本..." + cp .env.local.example .env.local + echo "⚠️ 請編輯 .env.local 檔案,填入你的 Supabase 配置" + else + echo "📝 創建環境變數檔案..." + cat > .env.local << EOF +# Supabase 配置 +NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key + +# 可選:Supabase Service Role Key +SUPABASE_SERVICE_ROLE_KEY=your_service_role_key +EOF + echo "⚠️ 請編輯 .env.local 檔案,填入你的 Supabase 配置" + fi +else + echo "✅ .env.local 檔案已存在" +fi + +# 檢查 SQL 腳本 +echo "🗄️ 檢查 SQL 腳本..." +sql_files=( + "scripts/01-create-tables.sql" + "scripts/02-create-indexes.sql" + "scripts/03-create-views-functions.sql" + "scripts/04-setup-storage.sql" + "scripts/05-setup-rls.sql" +) + +missing_files=() +for file in "${sql_files[@]}"; do + if [ ! -f "$file" ]; then + missing_files+=("$file") + fi +done + +if [ ${#missing_files[@]} -eq 0 ]; then + echo "✅ 所有 SQL 腳本檔案都存在" +else + echo "❌ 缺少以下 SQL 腳本檔案:" + for file in "${missing_files[@]}"; do + echo " - $file" + done +fi + +echo "" +echo "🎉 設置完成!" +echo "" +echo "📋 下一步操作:" +echo "1. 前往 https://supabase.com/dashboard 創建新項目" +echo "2. 編輯 .env.local 檔案,填入 Supabase 配置" +echo "3. 在 Supabase SQL Editor 中按順序執行 SQL 腳本:" +for i in "${!sql_files[@]}"; do + echo " $((i+1)). ${sql_files[$i]}" +done +echo "4. 執行 npm run dev 測試本地環境" +echo "5. 部署到 Vercel: npx vercel --prod" +echo "" +echo "📖 詳細說明請參考 SUPABASE-COMPLETE-SETUP.md" diff --git a/tailwind.config.ts b/tailwind.config.ts index c1a1a6c..49f5ef1 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,17 +1,14 @@ -import type { Config } from "tailwindcss" -import defaultConfig from "shadcn/ui/tailwind.config" - -const config: Config = { - ...defaultConfig, +/** @type {import('tailwindcss').Config} */ +const config = { + darkMode: ["class"], content: [ - ...defaultConfig.content, "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", "*.{js,ts,jsx,tsx,mdx}", ], + prefix: "", theme: { - ...defaultConfig.theme, container: { center: true, padding: "2rem", @@ -20,7 +17,6 @@ const config: Config = { }, }, extend: { - ...defaultConfig.theme.extend, colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", @@ -55,6 +51,23 @@ const config: Config = { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, + chart: { + "1": "hsl(var(--chart-1))", + "2": "hsl(var(--chart-2))", + "3": "hsl(var(--chart-3))", + "4": "hsl(var(--chart-4))", + "5": "hsl(var(--chart-5))", + }, }, borderRadius: { lg: "var(--radius)", @@ -173,7 +186,7 @@ const config: Config = { }, }, }, - plugins: [...defaultConfig.plugins, require("tailwindcss-animate")], -} satisfies Config + plugins: [require("tailwindcss-animate")], +} export default config