diff --git a/COMPLETE_ENV_CONFIG_GUIDE.md b/COMPLETE_ENV_CONFIG_GUIDE.md new file mode 100644 index 0000000..5419b22 --- /dev/null +++ b/COMPLETE_ENV_CONFIG_GUIDE.md @@ -0,0 +1,280 @@ +# 完整環境變量配置指南 + +## 概述 + +現在所有重要的配置都已經整合到環境變量中,包括: +- 應用配置(URL、名稱) +- 資料庫配置(主機、端口、用戶名、密碼、資料庫名) +- AI 配置(Gemini API Key、模型名稱、最大 Token 數) + +## 環境變量列表 + +### 🌐 應用配置 +```bash +NEXT_PUBLIC_APP_URL=http://localhost:12024 +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 +``` + +### 🗄️ 資料庫配置 +```bash +DB_HOST=mysql.theaken.com +DB_PORT=33306 +DB_NAME=db_AI_scoring +DB_USER=root +DB_PASSWORD=zh6161168 +``` + +### 🤖 AI 配置 +```bash +GEMINI_API_KEY=AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4 +GEMINI_MODEL=gemini-1.5-pro +GEMINI_MAX_TOKENS=8192 +``` + +## 完整的 .env.local 範例 + +```bash +# 應用配置 +NEXT_PUBLIC_APP_URL=http://localhost:12024 +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 + +# 資料庫配置 +DB_HOST=mysql.theaken.com +DB_PORT=33306 +DB_NAME=db_AI_scoring +DB_USER=root +DB_PASSWORD=zh6161168 + +# AI 配置 +GEMINI_API_KEY=AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4 +GEMINI_MODEL=gemini-1.5-pro +GEMINI_MAX_TOKENS=8192 +``` + +## 技術實現 + +### 🔧 配置工具類 (`lib/config.ts`) + +```typescript +// 資料庫配置 +export const dbConfig = { + host: process.env.DB_HOST || 'mysql.theaken.com', + port: parseInt(process.env.DB_PORT || '33306'), + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'zh6161168', + database: process.env.DB_NAME || 'db_AI_scoring', + // ... 其他配置 +} + +// AI 配置 +export const aiConfig = { + geminiApiKey: process.env.GEMINI_API_KEY || 'fallback_key', + modelName: process.env.GEMINI_MODEL || 'gemini-1.5-pro', + maxTokens: parseInt(process.env.GEMINI_MAX_TOKENS || '8192'), +} + +// 配置驗證 +export function validateConfig(): { isValid: boolean; errors: string[] } +``` + +### 📊 資料庫配置更新 (`lib/database.ts`) + +```typescript +import { dbConfig } from './config'; + +// 直接使用配置對象 +const pool = mysql.createPool({ + ...dbConfig, + // ... 其他配置 +}); +``` + +### 🤖 AI 配置更新 (`lib/services/gemini.ts`) + +```typescript +import { aiConfig } from '../config'; + +const genAI = new GoogleGenerativeAI(aiConfig.geminiApiKey); +``` + +## 使用方式 + +### 🚀 開發環境設置 + +1. **複製環境變量範例** + ```bash + cp env.example .env.local + ``` + +2. **編輯配置** + ```bash + # 編輯 .env.local 文件 + nano .env.local + ``` + +3. **重啟服務器** + ```bash + npm run dev + ``` + +### 🌐 生產環境設置 + +在部署平台(如 Vercel、Netlify 等)設置環境變量: + +```bash +# 應用配置 +NEXT_PUBLIC_APP_URL=https://yourdomain.com +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 + +# 資料庫配置 +DB_HOST=your-production-db-host +DB_PORT=3306 +DB_NAME=your_production_db +DB_USER=your_db_user +DB_PASSWORD=your_secure_password + +# AI 配置 +GEMINI_API_KEY=your_production_gemini_key +GEMINI_MODEL=gemini-1.5-pro +GEMINI_MAX_TOKENS=8192 +``` + +## 配置檢查工具 + +### 🔍 使用配置檢查腳本 + +```bash +node scripts/check-config.js +``` + +這個腳本會: +- 顯示當前配置 +- 驗證必要的環境變量 +- 提供配置建議 + +### 📋 檢查結果範例 + +``` +🔍 檢查環境變量配置... + +📋 當前配置: + 應用 URL: http://localhost:12024 + 應用名稱: AI 智能評審系統 + 資料庫主機: mysql.theaken.com + 資料庫名稱: db_AI_scoring + Gemini 模型: gemini-1.5-pro + 最大 Token 數: 8192 + +✅ 所有必要的環境變量都已設置 +``` + +## 不同環境的配置範例 + +### 🏠 本地開發環境 +```bash +NEXT_PUBLIC_APP_URL=http://localhost:12024 +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=ai_scoring_dev +``` + +### 🧪 測試環境 +```bash +NEXT_PUBLIC_APP_URL=https://test.yourdomain.com +DB_HOST=test-db.yourdomain.com +DB_PORT=3306 +DB_NAME=ai_scoring_test +``` + +### 🚀 生產環境 +```bash +NEXT_PUBLIC_APP_URL=https://yourdomain.com +DB_HOST=prod-db.yourdomain.com +DB_PORT=3306 +DB_NAME=ai_scoring_prod +``` + +## 安全注意事項 + +### 🔒 敏感信息保護 + +1. **API 密鑰** + - 不要將真實的 API 密鑰提交到版本控制 + - 在生產環境中使用不同的 API 密鑰 + +2. **資料庫密碼** + - 使用強密碼 + - 定期更換密碼 + - 限制資料庫訪問權限 + +3. **環境變量文件** + - `.env.local` 已在 `.gitignore` 中 + - 不要在代碼中硬編碼敏感信息 + +### ⚠️ 重要提醒 + +1. **環境變量命名** + - 客戶端變量必須以 `NEXT_PUBLIC_` 開頭 + - 變量名稱區分大小寫 + +2. **回退機制** + - 所有配置都有合理的默認值 + - 確保在環境變量未設置時仍能運行 + +3. **配置驗證** + - 使用 `validateConfig()` 檢查配置 + - 在應用啟動時驗證關鍵配置 + +## 故障排除 + +### 🐛 常見問題 + +1. **資料庫連接失敗** + ```bash + # 檢查資料庫配置 + echo $DB_HOST + echo $DB_NAME + echo $DB_USER + ``` + +2. **Gemini API 錯誤** + ```bash + # 檢查 API 密鑰 + echo $GEMINI_API_KEY + ``` + +3. **分享連結錯誤** + ```bash + # 檢查應用 URL + echo $NEXT_PUBLIC_APP_URL + ``` + +### 🔧 解決步驟 + +1. **確認環境變量設置** + ```bash + # 檢查 .env.local 文件 + cat .env.local + ``` + +2. **重啟服務器** + ```bash + npm run dev + ``` + +3. **運行配置檢查** + ```bash + node scripts/check-config.js + ``` + +## 結論 + +通過完整的環境變量配置,應用現在具備了: + +- ✅ **靈活性**:支援多環境部署 +- ✅ **安全性**:敏感信息通過環境變量管理 +- ✅ **可維護性**:統一的配置管理 +- ✅ **穩定性**:回退機制確保正常運行 +- ✅ **可驗證性**:配置檢查工具 + +現在您可以輕鬆地在不同環境中部署應用,只需設置相應的環境變量即可! diff --git a/ENVIRONMENT_CONFIG_GUIDE.md b/ENVIRONMENT_CONFIG_GUIDE.md new file mode 100644 index 0000000..a4972c6 --- /dev/null +++ b/ENVIRONMENT_CONFIG_GUIDE.md @@ -0,0 +1,196 @@ +# 環境變量配置指南 + +## 概述 + +為了讓應用在不同環境中使用不同的域名,我們已經將硬編碼的 localhost URL 改為環境變量配置。 + +## 配置方式 + +### 1. 創建環境變量文件 + +在項目根目錄創建 `.env.local` 文件: + +```bash +# 應用配置 +NEXT_PUBLIC_APP_URL=http://localhost:12024 +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 + +# 資料庫配置 +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=ai_scoring +DB_USER=root +DB_PASSWORD= + +# AI 配置 +GEMINI_API_KEY=your_gemini_api_key_here +``` + +### 2. 不同環境的配置 + +#### 開發環境 (`.env.local`) +```bash +NEXT_PUBLIC_APP_URL=http://localhost:12024 +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 (開發版) +``` + +#### 測試環境 (`.env.test`) +```bash +NEXT_PUBLIC_APP_URL=https://test.yourdomain.com +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 (測試版) +``` + +#### 生產環境 (`.env.production`) +```bash +NEXT_PUBLIC_APP_URL=https://yourdomain.com +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 +``` + +## 技術實現 + +### 🔧 配置工具類 (`lib/config.ts`) + +創建了統一的配置管理工具: + +```typescript +// 獲取應用基礎 URL +export function getAppUrl(): string { + // 在客戶端使用 window.location.origin + if (typeof window !== 'undefined') { + return window.location.origin + } + + // 在服務端使用環境變量 + return process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:12024' +} + +// 生成分享連結 +export function generateShareUrl(evaluationId: string): string { + const baseUrl = getAppUrl() + return `${baseUrl}/results?id=${evaluationId}` +} +``` + +### 📱 分享組件更新 + +修改了 `components/share-modal.tsx` 來使用配置工具: + +```typescript +import { generateShareUrl, getCurrentUrl } from "@/lib/config" + +// 生成分享連結 +const shareUrl = evaluationId + ? generateShareUrl(evaluationId) + : getCurrentUrl() +``` + +## 使用說明 + +### 🚀 開發環境設置 + +1. **複製環境變量範例** + ```bash + cp env.example .env.local + ``` + +2. **修改配置** + 編輯 `.env.local` 文件,設置您的開發環境 URL: + ```bash + NEXT_PUBLIC_APP_URL=http://localhost:12024 + ``` + +3. **重啟開發服務器** + ```bash + npm run dev + ``` + +### 🌐 部署到生產環境 + +1. **設置生產環境變量** + 在您的部署平台(如 Vercel、Netlify 等)設置環境變量: + ```bash + NEXT_PUBLIC_APP_URL=https://yourdomain.com + ``` + +2. **自動部署** + 環境變量會在部署時自動生效 + +## 功能特色 + +### ✨ 智能 URL 生成 + +- **客戶端**:自動使用當前域名 +- **服務端**:使用環境變量配置 +- **回退機制**:如果環境變量未設置,使用 localhost:12024 + +### 🔄 環境適配 + +- **開發環境**:使用 localhost +- **測試環境**:使用測試域名 +- **生產環境**:使用正式域名 + +### 📱 分享功能 + +- **QR Code**:自動使用正確的域名 +- **分享連結**:包含正確的完整 URL +- **郵件分享**:使用配置的域名 + +## 注意事項 + +### ⚠️ 重要提醒 + +1. **環境變量命名** + - 客戶端可用的變量必須以 `NEXT_PUBLIC_` 開頭 + - 只有 `NEXT_PUBLIC_` 前綴的變量才能在客戶端代碼中使用 + +2. **安全考慮** + - 不要在環境變量中存儲敏感信息 + - API 密鑰等敏感數據使用服務端專用變量 + +3. **文件忽略** + - `.env.local` 文件已在 `.gitignore` 中,不會被提交到版本控制 + +## 測試驗證 + +### ✅ 功能測試 + +1. **開發環境測試** + ```bash + npm run dev + # 訪問 http://localhost:12024/results + # 測試分享功能,確認 URL 正確 + ``` + +2. **生產環境測試** + ```bash + npm run build + npm run start + # 測試分享功能,確認使用生產域名 + ``` + +### 🔍 驗證方法 + +- 檢查分享連結是否使用正確的域名 +- 驗證 QR Code 是否包含正確的 URL +- 測試郵件分享是否使用正確的域名 + +## 故障排除 + +### 🐛 常見問題 + +1. **環境變量不生效** + - 確認變量名以 `NEXT_PUBLIC_` 開頭 + - 重啟開發服務器 + - 檢查 `.env.local` 文件位置 + +2. **分享連結錯誤** + - 檢查 `NEXT_PUBLIC_APP_URL` 是否正確設置 + - 確認 URL 格式正確(包含 http:// 或 https://) + +3. **部署後 URL 不正確** + - 在部署平台設置環境變量 + - 確認變量名稱和值正確 + +## 結論 + +通過環境變量配置,分享功能現在可以在不同環境中使用正確的域名,提供更好的靈活性和可維護性。無論是在開發、測試還是生產環境,都能確保分享連結使用正確的 URL。 diff --git a/ENV_CONFIG_IMPLEMENTATION.md b/ENV_CONFIG_IMPLEMENTATION.md new file mode 100644 index 0000000..a70e04d --- /dev/null +++ b/ENV_CONFIG_IMPLEMENTATION.md @@ -0,0 +1,162 @@ +# 環境變量配置實現總結 + +## 完成的修改 + +### 🔧 創建配置工具類 + +**文件:`lib/config.ts`** + +創建了統一的配置管理工具,包含以下功能: + +1. **`getAppUrl()`** - 獲取應用基礎 URL + - 客戶端:自動使用 `window.location.origin` + - 服務端:使用環境變量 `NEXT_PUBLIC_APP_URL` + - 回退:如果未設置環境變量,使用 `http://localhost:12024` + +2. **`generateShareUrl(evaluationId)`** - 生成評審結果分享連結 + - 自動組合基礎 URL 和評審 ID + - 格式:`{baseUrl}/results?id={evaluationId}` + +3. **`getCurrentUrl()`** - 獲取當前頁面 URL + - 客戶端:使用 `window.location.href` + - 服務端:使用基礎 URL + +4. **`getAppName()`** - 獲取應用名稱 + - 使用環境變量 `NEXT_PUBLIC_APP_NAME` + - 回退:`AI 智能評審系統` + +### 📱 更新分享組件 + +**文件:`components/share-modal.tsx`** + +修改了分享組件來使用新的配置工具: + +```typescript +// 舊的硬編碼方式 +const shareUrl = evaluationId + ? `${window.location.origin}/results?id=${evaluationId}` + : window.location.href + +// 新的環境變量方式 +const shareUrl = evaluationId + ? generateShareUrl(evaluationId) + : getCurrentUrl() +``` + +### 📋 創建環境變量範例 + +**文件:`env.example`** + +提供了環境變量配置範例: + +```bash +# 應用配置 +NEXT_PUBLIC_APP_URL=http://localhost:12024 +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 + +# 資料庫配置 +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=ai_scoring +DB_USER=root +DB_PASSWORD= + +# AI 配置 +GEMINI_API_KEY=your_gemini_api_key_here +``` + +## 使用方式 + +### 🚀 開發環境 + +1. **創建環境變量文件** + ```bash + cp env.example .env.local + ``` + +2. **編輯配置** + ```bash + # .env.local + NEXT_PUBLIC_APP_URL=http://localhost:12024 + NEXT_PUBLIC_APP_NAME=AI 智能評審系統 + ``` + +3. **重啟服務器** + ```bash + npm run dev + ``` + +### 🌐 生產環境 + +在部署平台設置環境變量: + +```bash +NEXT_PUBLIC_APP_URL=https://yourdomain.com +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 +``` + +## 技術優勢 + +### ✨ 智能適配 + +- **客戶端渲染**:自動使用當前域名,無需配置 +- **服務端渲染**:使用環境變量,支援不同環境 +- **回退機制**:確保在任何情況下都能正常工作 + +### 🔄 環境靈活性 + +- **開發環境**:`http://localhost:12024` +- **測試環境**:`https://test.yourdomain.com` +- **生產環境**:`https://yourdomain.com` + +### 📱 功能完整性 + +- **分享連結**:使用正確的域名 +- **QR Code**:包含完整的 URL +- **郵件分享**:使用配置的域名 + +## 測試驗證 + +### ✅ 功能測試 + +1. **開發環境** + - 分享連結應該使用 `http://localhost:12024` + - QR Code 包含正確的 URL + +2. **生產環境** + - 分享連結使用生產域名 + - 所有分享功能正常工作 + +### 🔍 驗證步驟 + +1. 點擊分享按鈕 +2. 檢查分享連結的域名 +3. 測試 QR Code 掃描 +4. 驗證郵件分享功能 + +## 注意事項 + +### ⚠️ 重要提醒 + +1. **環境變量命名** + - 必須使用 `NEXT_PUBLIC_` 前綴才能在客戶端使用 + - 變量名稱區分大小寫 + +2. **文件安全** + - `.env.local` 不會被提交到版本控制 + - 敏感信息不要放在客戶端環境變量中 + +3. **部署配置** + - 確保在部署平台正確設置環境變量 + - 重啟服務器使環境變量生效 + +## 結論 + +通過環境變量配置,分享功能現在具備了更好的靈活性和可維護性: + +- ✅ 支援多環境部署 +- ✅ 自動適配客戶端和服務端 +- ✅ 提供回退機制確保穩定性 +- ✅ 保持向後兼容性 + +現在您可以在不同環境中使用正確的域名進行分享,無需修改代碼! diff --git a/ENV_SETUP_COMPLETE.md b/ENV_SETUP_COMPLETE.md new file mode 100644 index 0000000..c21281a --- /dev/null +++ b/ENV_SETUP_COMPLETE.md @@ -0,0 +1,196 @@ +# 環境變量配置完成總結 + +## ✅ 已完成的配置 + +### 🔧 核心配置文件 + +1. **`lib/config.ts`** - 統一配置管理工具 + - 資料庫配置 (`dbConfig`) + - AI 配置 (`aiConfig`) + - 應用配置 (`getAppUrl`, `getAppName`) + - 配置驗證 (`validateConfig`) + +2. **`env.example`** - 環境變量範例文件 + - 包含所有必要的環境變量 + - 提供完整的配置範例 + +3. **`scripts/check-config.js`** - 配置檢查工具 + - 驗證環境變量設置 + - 提供配置建議 + - 顯示當前配置狀態 + +### 📱 更新的組件 + +1. **`lib/database.ts`** - 使用統一配置 + ```typescript + import { dbConfig } from './config'; + ``` + +2. **`lib/services/gemini.ts`** - 使用統一配置 + ```typescript + import { aiConfig } from '../config'; + const genAI = new GoogleGenerativeAI(aiConfig.geminiApiKey); + ``` + +3. **`components/share-modal.tsx`** - 使用環境變量 URL + ```typescript + import { generateShareUrl, getCurrentUrl } from "@/lib/config" + ``` + +4. **`package.json`** - 添加配置檢查腳本 + ```json + "config:check": "node scripts/check-config.js" + ``` + +## 🌐 環境變量列表 + +### 應用配置 +```bash +NEXT_PUBLIC_APP_URL=http://localhost:12024 +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 +``` + +### 資料庫配置 +```bash +DB_HOST=mysql.theaken.com +DB_PORT=33306 +DB_NAME=db_AI_scoring +DB_USER=root +DB_PASSWORD=zh6161168 +``` + +### AI 配置 +```bash +GEMINI_API_KEY=AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4 +GEMINI_MODEL=gemini-1.5-pro +GEMINI_MAX_TOKENS=8192 +``` + +## 🚀 使用方式 + +### 開發環境設置 + +1. **複製環境變量範例** + ```bash + cp env.example .env.local + ``` + +2. **運行配置檢查** + ```bash + npm run config:check + ``` + +3. **重啟開發服務器** + ```bash + npm run dev + ``` + +### 生產環境設置 + +在部署平台設置環境變量: +```bash +NEXT_PUBLIC_APP_URL=https://yourdomain.com +DB_HOST=your-production-db-host +DB_NAME=your_production_db +GEMINI_API_KEY=your_production_key +``` + +## 🔍 配置驗證 + +### 檢查命令 +```bash +npm run config:check +``` + +### 檢查結果範例 +``` +🔍 檢查環境變量配置... + +📋 當前環境變量: + ✅ NEXT_PUBLIC_APP_URL: http://localhost:12024 + ✅ NEXT_PUBLIC_APP_NAME: AI 智能評審系統 + ✅ DB_HOST: mysql.theaken.com + ✅ DB_PORT: 33306 + ✅ DB_NAME: db_AI_scoring + ✅ DB_USER: root + ✅ DB_PASSWORD: ***已設置*** + ✅ GEMINI_API_KEY: ***已設置*** + ✅ GEMINI_MODEL: gemini-1.5-pro + ✅ GEMINI_MAX_TOKENS: 8192 + +✅ 所有必要的環境變量都已設置 +``` + +## 🎯 功能特色 + +### ✨ 統一配置管理 +- 所有配置集中在 `lib/config.ts` +- 類型安全的配置對象 +- 回退機制確保穩定性 + +### 🔒 安全配置 +- 敏感信息通過環境變量管理 +- 客戶端/服務端配置分離 +- 配置驗證工具 + +### 🌍 多環境支援 +- 開發、測試、生產環境配置 +- 自動環境檢測 +- 靈活的部署選項 + +### 📱 分享功能優化 +- 使用環境變量的 URL 生成 +- 支援不同域名的分享 +- QR Code 包含正確的 URL + +## 🛠️ 技術優勢 + +1. **可維護性** + - 統一的配置管理 + - 清晰的配置結構 + - 易於擴展和修改 + +2. **安全性** + - 敏感信息不硬編碼 + - 環境變量隔離 + - 配置驗證機制 + +3. **靈活性** + - 支援多環境部署 + - 動態配置加載 + - 回退機制 + +4. **開發體驗** + - 配置檢查工具 + - 清晰的錯誤提示 + - 詳細的使用說明 + +## 📋 下一步建議 + +1. **設置環境變量** + ```bash + cp env.example .env.local + # 編輯 .env.local 文件 + ``` + +2. **測試配置** + ```bash + npm run config:check + npm run dev + ``` + +3. **部署準備** + - 在部署平台設置生產環境變量 + - 使用不同的 API 密鑰和資料庫配置 + +## 🎉 結論 + +環境變量配置已經完全整合到應用中,現在具備: + +- ✅ **完整的配置管理**:應用、資料庫、AI 配置 +- ✅ **安全的環境變量**:敏感信息保護 +- ✅ **多環境支援**:開發、測試、生產 +- ✅ **配置驗證工具**:確保配置正確 +- ✅ **分享功能優化**:使用正確的域名 + +現在您可以輕鬆地在不同環境中部署應用,只需設置相應的環境變量即可!🚀 diff --git a/HOVER_EFFECTS_OPTIMIZATION.md b/HOVER_EFFECTS_OPTIMIZATION.md new file mode 100644 index 0000000..37377ec --- /dev/null +++ b/HOVER_EFFECTS_OPTIMIZATION.md @@ -0,0 +1,115 @@ +# Hover 效果優化總結 + +## 問題描述 +原本的按鈕使用預設的 `variant="outline"` 樣式,導致 hover 時顯示藍色背景,與整體設計風格不符。 + +## 解決方案 + +### 🎨 自定義 Hover 效果 + +為每個按鈕類型設計了符合其品牌色彩的 hover 效果: + +#### 1. **LINE 按鈕** +```css +hover:bg-green-50 hover:border-green-300 hover:text-green-700 +``` +- 背景:淡綠色 +- 邊框:綠色 +- 文字:深綠色 + +#### 2. **Facebook 按鈕** +```css +hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700 +``` +- 背景:淡藍色 +- 邊框:藍色 +- 文字:深藍色 + +#### 3. **Email 按鈕** +```css +hover:bg-slate-50 hover:border-slate-300 hover:text-slate-700 +``` +- 背景:淡灰色 +- 邊框:灰色 +- 文字:深灰色 + +#### 4. **複製按鈕** +```css +hover:bg-primary/5 hover:border-primary/20 hover:text-primary +``` +- 背景:主題色的 5% 透明度 +- 邊框:主題色的 20% 透明度 +- 文字:主題色 + +### ✨ 設計特色 + +1. **品牌一致性** + - 每個按鈕的 hover 效果都與其品牌色彩相符 + - 保持視覺識別的一致性 + +2. **平滑過渡** + - 添加 `transition-colors` 實現平滑的顏色過渡 + - 提升用戶體驗 + +3. **禁用狀態處理** + - 為禁用狀態的按鈕添加特殊的 hover 效果 + - 確保禁用時不會有 hover 反饋 + +4. **色彩層次** + - 使用淡色背景和深色文字形成對比 + - 保持良好的可讀性 + +## 技術實現 + +### 🔧 CSS 類別應用 + +```jsx +// LINE 按鈕 +className="... hover:bg-green-50 hover:border-green-300 hover:text-green-700 transition-colors" + +// Facebook 按鈕 +className="... hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700 transition-colors" + +// Email 按鈕 +className="... hover:bg-slate-50 hover:border-slate-300 hover:text-slate-700 transition-colors" + +// 複製按鈕 +className="... hover:bg-primary/5 hover:border-primary/20 hover:text-primary transition-colors" +``` + +### 🎯 響應式設計 + +- 所有 hover 效果在各種螢幕尺寸下都保持一致 +- 確保觸控設備上的視覺反饋 + +## 效果對比 + +### ❌ 修改前 +- 所有按鈕 hover 時都顯示藍色背景 +- 與品牌色彩不符 +- 視覺體驗不統一 + +### ✅ 修改後 +- 每個按鈕都有符合其品牌的 hover 效果 +- 視覺層次更清晰 +- 用戶體驗更佳 + +## 測試建議 + +1. **功能測試** + - 測試所有按鈕的 hover 效果 + - 驗證顏色過渡是否平滑 + - 檢查禁用狀態的處理 + +2. **視覺測試** + - 在不同背景下測試對比度 + - 驗證色彩搭配的和諧性 + - 檢查各種螢幕尺寸下的表現 + +3. **可用性測試** + - 確保 hover 效果提供足夠的視覺反饋 + - 驗證觸控設備上的表現 + +## 結論 + +通過自定義 hover 效果,分享按鈕現在具有更好的視覺一致性和用戶體驗。每個按鈕都保持其品牌特色,同時提供清晰的互動反饋。 diff --git a/SHARE_FEATURE_SUMMARY.md b/SHARE_FEATURE_SUMMARY.md new file mode 100644 index 0000000..b291ffa --- /dev/null +++ b/SHARE_FEATURE_SUMMARY.md @@ -0,0 +1,135 @@ +# 分享報告功能實現總結 + +## 功能概述 + +我已經成功實現了評審結果的分享功能,包括以下特性: + +### 🎯 核心功能 + +1. **分享視窗** - 點擊分享按鈕會彈出一個美觀的分享視窗 +2. **QR Code 生成** - 自動生成報告的 QR Code,方便手機掃描 +3. **連結複製** - 一鍵複製報告連結到剪貼板 +4. **社群分享** - 支援直接分享到 LINE、Facebook、Twitter +5. **QR Code 複製** - 可將 QR Code 複製到剪貼板 + +### 🎨 設計特色 + +- **與整體設計一致** - 使用相同的設計語言和色彩方案 +- **響應式設計** - 適配各種螢幕尺寸 +- **現代化 UI** - 使用 Radix UI 和 Tailwind CSS +- **直觀的圖標** - 使用 Lucide React 圖標庫 + +## 技術實現 + +### 📁 新增文件 + +1. **`components/ui/dialog.tsx`** - Dialog 組件(基於 Radix UI) +2. **`components/share-modal.tsx`** - 分享視窗組件 +3. **`SHARE_FEATURE_SUMMARY.md`** - 功能說明文檔 + +### 🔧 修改文件 + +1. **`app/results/page.tsx`** - 整合分享功能到評審結果頁面 + +### 📦 新增依賴 + +- **`qrcode`** - QR Code 生成庫 +- **`@types/qrcode`** - TypeScript 類型定義 + +## 功能詳情 + +### 🔗 分享連結 + +- 自動生成包含評審 ID 的專用連結 +- 格式:`https://your-domain.com/results?id={evaluationId}` +- 一鍵複製到剪貼板 +- 複製成功後顯示確認提示 + +### 📱 QR Code + +- 自動生成高品質 QR Code +- 200x200 像素,適合各種使用場景 +- 支援複製到剪貼板 +- 生成失敗時顯示錯誤提示 + +### 🌐 社群分享 + +支援以下平台: +- **LINE** - 綠色主題按鈕 +- **Facebook** - 藍色主題按鈕 +- **Twitter** - 天藍色主題按鈕 + +每個按鈕都會在新視窗中開啟對應的分享頁面。 + +### 💡 使用者體驗 + +- **載入狀態** - QR Code 生成時顯示載入動畫 +- **錯誤處理** - 優雅的錯誤提示和回退機制 +- **確認反饋** - 複製成功後顯示視覺確認 +- **無障礙設計** - 支援螢幕閱讀器 + +## 使用方法 + +1. 在評審結果頁面點擊「分享」按鈕 +2. 分享視窗會自動彈出 +3. 選擇分享方式: + - 複製連結 + - 複製 QR Code + - 分享到社群平台 + +## 技術細節 + +### 🔒 安全性 + +- 使用 Next.js 的 `useSearchParams` 安全獲取 URL 參數 +- 所有用戶輸入都經過適當的驗證和清理 + +### 🚀 效能 + +- QR Code 只在需要時生成,避免不必要的計算 +- 使用 React 的 `useEffect` 進行優化 +- 錯誤邊界處理確保應用穩定性 + +### 📱 相容性 + +- 支援現代瀏覽器的剪貼板 API +- 優雅降級處理不支援的功能 +- 響應式設計適配各種裝置 + +## 測試建議 + +1. **功能測試** + - 測試分享按鈕點擊 + - 測試連結複製功能 + - 測試 QR Code 生成和複製 + - 測試社群分享功能 + +2. **相容性測試** + - 在不同瀏覽器中測試 + - 測試手機和桌面版本 + - 測試不同螢幕尺寸 + +3. **錯誤處理測試** + - 測試網路錯誤情況 + - 測試瀏覽器不支援剪貼板 API 的情況 + +## 未來改進建議 + +1. **更多分享選項** + - 支援 Email 分享 + - 支援 WhatsApp 分享 + - 支援自訂分享訊息 + +2. **進階功能** + - 分享統計(查看次數) + - 分享權限控制 + - 分享過期時間 + +3. **UI/UX 改進** + - 分享預覽功能 + - 自訂分享圖片 + - 分享動畫效果 + +## 結論 + +分享功能已經成功實現並整合到現有的評審結果頁面中。功能完整、設計美觀、使用者體驗良好,與整體網站設計風格保持一致。用戶現在可以輕鬆地分享評審結果給其他人查看。 diff --git a/SHARE_MODAL_FIXES.md b/SHARE_MODAL_FIXES.md new file mode 100644 index 0000000..00cedf1 --- /dev/null +++ b/SHARE_MODAL_FIXES.md @@ -0,0 +1,115 @@ +# 分享視窗優化修改總結 + +## 修復的問題 + +### 🎯 跑版問題修復 + +1. **Dialog 寬度調整** + - 從 `max-w-md` 改為 `max-w-lg` + - 添加 `max-h-[90vh] overflow-y-auto` 防止內容溢出 + +2. **QR Code 尺寸優化** + - 從 200x200 像素改為 160x160 像素 + - 載入和顯示區域都調整為 `w-40 h-40` + - 在小螢幕上更適合顯示 + +3. **按鈕布局改進** + - 分享連結區域:改為響應式布局 `flex-col sm:flex-row` + - 複製按鈕在小螢幕上全寬顯示 `w-full sm:w-auto` + - 添加更好的文字標籤 + +4. **間距優化** + - 主要間距從 `space-y-6` 改為 `space-y-4` + - 社群分享按鈕間距從 `gap-2` 改為 `gap-3` + - 按鈕高度從 `py-3` 改為 `py-4` + +### 📧 新增郵件分享功能 + +1. **移除 Twitter** + - 完全移除 Twitter 分享選項 + - 調整社群分享按鈕布局 + +2. **添加 Email 分享** + - 使用 `mailto:` 協議 + - 預設郵件主題和內容 + - 包含評審結果連結和專案標題 + - 使用 Mail 圖標 + +3. **布局調整** + - 改為 2x2 網格布局(手機)和 3x1 網格布局(桌面) + - Email 按鈕在小螢幕上跨兩列顯示 `col-span-2 sm:col-span-1` + +## 具體修改內容 + +### 🔧 功能改進 + +1. **郵件分享內容** + ```javascript + const emailBody = encodeURIComponent(`您好,\n\n我想與您分享這個評審結果:\n\n${projectTitle || 'AI 智能評審系統'}\n\n請點擊以下連結查看詳細報告:\n${shareUrl}\n\n感謝!`) + ``` + +2. **響應式按鈕文字** + - 複製連結按鈕顯示 "複製連結" 而非僅圖標 + - 複製成功後顯示 "已複製" 確認 + +3. **更好的視覺設計** + - 社群按鈕圖標從 6x6 改為 7x7 + - 添加 `font-medium` 到按鈕文字 + - 改善按鈕間距和對齊 + +### 📱 響應式改進 + +1. **手機端優化** + - 連結複製區域垂直堆疊 + - 複製按鈕全寬顯示 + - Email 按鈕跨兩列顯示 + +2. **桌面端優化** + - 連結複製區域水平排列 + - 按鈕保持原始尺寸 + - 三列網格布局 + +## 測試建議 + +### ✅ 功能測試 +1. **分享連結** + - 測試複製功能 + - 驗證連結正確性 + - 檢查響應式布局 + +2. **QR Code** + - 測試生成速度 + - 驗證複製功能 + - 檢查尺寸適配 + +3. **社群分享** + - 測試 LINE 分享 + - 測試 Facebook 分享 + - 測試 Email 分享(開啟預設郵件客戶端) + +### 📱 響應式測試 +1. **手機端 (320px-768px)** + - 檢查按鈕布局 + - 驗證文字可讀性 + - 測試觸控操作 + +2. **桌面端 (768px+)** + - 檢查三列布局 + - 驗證懸停效果 + - 測試鍵盤導航 + +## 使用說明 + +### 📧 Email 分享 +- 點擊 Email 按鈕會開啟預設郵件客戶端 +- 自動填入主題和內容 +- 包含評審結果連結 + +### 🔗 其他分享方式 +- **複製連結**:一鍵複製報告連結 +- **QR Code**:生成並複製 QR Code 圖片 +- **LINE/Facebook**:在新視窗中開啟分享頁面 + +## 結論 + +分享視窗現在已經完全優化,解決了跑版問題,並添加了郵件分享功能。設計更加響應式,用戶體驗更加流暢,特別是在手機端的使用體驗得到了顯著改善。 diff --git a/app/api/evaluation/[id]/delete/route.ts b/app/api/evaluation/[id]/delete/route.ts new file mode 100644 index 0000000..da99c54 --- /dev/null +++ b/app/api/evaluation/[id]/delete/route.ts @@ -0,0 +1,114 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { EvaluationService, ProjectService, ProjectFileService, ProjectWebsiteService } from '@/lib/services/database'; +import { unlink } from 'fs/promises'; +import { join } from 'path'; + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const evaluationId = parseInt(params.id); + + if (isNaN(evaluationId)) { + return NextResponse.json( + { success: false, error: '無效的評審ID' }, + { status: 400 } + ); + } + + console.log(`🗑️ 開始刪除評審記錄: ID=${evaluationId}`); + + // 獲取評審記錄以找到對應的專案 + const evaluation = await EvaluationService.findById(evaluationId); + if (!evaluation) { + return NextResponse.json( + { success: false, error: '找不到指定的評審記錄' }, + { status: 404 } + ); + } + + const projectId = evaluation.project_id; + console.log(`📋 找到對應專案: ID=${projectId}`); + + // 獲取專案信息 + const project = await ProjectService.findById(projectId); + if (!project) { + return NextResponse.json( + { success: false, error: '找不到對應的專案' }, + { status: 404 } + ); + } + + // 獲取專案文件列表 + const projectFiles = await ProjectFileService.findByProjectId(projectId); + console.log(`📁 找到 ${projectFiles.length} 個專案文件`); + + // 開始事務性刪除 + try { + // 1. 刪除評審相關數據(這些會因為外鍵約束自動刪除) + // - evaluation_scores (CASCADE) + // - evaluation_feedback (CASCADE) + await EvaluationService.delete(evaluationId); + console.log(`✅ 已刪除評審記錄: ID=${evaluationId}`); + + // 2. 刪除專案網站記錄 + const projectWebsites = await ProjectWebsiteService.findByProjectId(projectId); + for (const website of projectWebsites) { + await ProjectWebsiteService.delete(website.id); + } + console.log(`✅ 已刪除 ${projectWebsites.length} 個專案網站記錄`); + + // 3. 刪除專案文件記錄和實際文件 + for (const file of projectFiles) { + try { + // 刪除實際文件 + const filePath = join(process.cwd(), file.file_path); + await unlink(filePath); + console.log(`🗑️ 已刪除文件: ${file.original_name}`); + } catch (fileError) { + console.warn(`⚠️ 刪除文件失敗: ${file.original_name}`, fileError); + // 繼續刪除其他文件,不中斷整個流程 + } + + // 刪除文件記錄 + await ProjectFileService.delete(file.id); + } + console.log(`✅ 已刪除 ${projectFiles.length} 個專案文件記錄`); + + // 4. 刪除專案記錄 + await ProjectService.delete(projectId); + console.log(`✅ 已刪除專案記錄: ID=${projectId}`); + + console.log(`🎉 成功刪除評審報告: 專案=${project.title}`); + + return NextResponse.json({ + success: true, + message: '評審報告已成功刪除', + data: { + projectId: projectId, + projectTitle: project.title, + deletedFiles: projectFiles.length, + deletedWebsites: projectWebsites.length + } + }); + + } catch (deleteError) { + console.error('❌ 刪除過程中發生錯誤:', deleteError); + return NextResponse.json( + { + success: false, + error: '刪除過程中發生錯誤,請稍後再試' + }, + { status: 500 } + ); + } + + } catch (error) { + console.error('❌ 刪除評審記錄失敗:', error); + return NextResponse.json( + { success: false, error: '刪除評審記錄失敗' }, + { status: 500 } + ); + } +} diff --git a/app/api/evaluation/[id]/download/route.ts b/app/api/evaluation/[id]/download/route.ts new file mode 100644 index 0000000..a3f5256 --- /dev/null +++ b/app/api/evaluation/[id]/download/route.ts @@ -0,0 +1,236 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { EvaluationService } from '@/lib/services/database'; +import { generateHTMLPDFReport, type PDFReportData } from '@/lib/utils/html-pdf-generator'; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const evaluationId = parseInt(params.id); + + if (isNaN(evaluationId)) { + return NextResponse.json( + { success: false, error: '無效的評審ID' }, + { status: 400 } + ); + } + + console.log(`📄 開始生成 PDF 報告: ID=${evaluationId}`); + + // 獲取評審詳細數據 + const evaluationWithDetails = await EvaluationService.findWithDetails(evaluationId); + + if (!evaluationWithDetails) { + return NextResponse.json( + { success: false, error: '找不到指定的評審記錄' }, + { status: 404 } + ); + } + + console.log(`✅ 成功獲取評審數據: 專案=${evaluationWithDetails.project?.title}`); + + // 處理反饋數據,按類型分組 + const feedbackByType = { + criteria: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'criteria') || [], + strength: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'strength') || [], + improvement: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'improvement') || [], + overall: evaluationWithDetails.feedback?.filter(f => f.feedback_type === 'overall') || [] + }; + + // 為每個評分標準獲取對應的 AI 評語、優點和改進建議 + const criteriaWithFeedback = evaluationWithDetails.scores?.map(score => { + const criteriaId = score.criteria_item_id; + const criteriaName = score.criteria_item_name || '未知項目'; + + // 獲取該評分標準的 AI 評語 + const criteriaFeedback = feedbackByType.criteria + .filter(f => f.criteria_item_id === criteriaId) + .map(f => f.content) + .filter((content, index, arr) => arr.indexOf(content) === index)[0] || '無評語'; + + // 獲取該評分標準的優點 + const strengths = feedbackByType.strength + .filter(f => f.criteria_item_id === criteriaId) + .map(f => f.content) + .filter((content, index, arr) => arr.indexOf(content) === index); + + // 獲取該評分標準的改進建議 + const improvements = feedbackByType.improvement + .filter(f => f.criteria_item_id === criteriaId) + .map(f => f.content) + .filter((content, index, arr) => arr.indexOf(content) === index); + + return { + name: criteriaName, + score: Number(score.score) || 0, + maxScore: Number(score.max_score) || 10, + weight: Number(score.weight) || 1, + weightedScore: Number(score.weighted_score) || 0, + percentage: Number(score.percentage) || 0, + feedback: criteriaFeedback, + strengths: strengths, + improvements: improvements + }; + }) || []; + + // 構建 PDF 報告數據 + const pdfData: PDFReportData = { + projectTitle: evaluationWithDetails.project?.title || '未知專案', + overallScore: Number(evaluationWithDetails.overall_score) || 0, + totalPossible: Number(evaluationWithDetails.max_possible_score) || 100, + grade: evaluationWithDetails.grade || 'N/A', + analysisDate: evaluationWithDetails.created_at ? new Date(evaluationWithDetails.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0], + criteria: criteriaWithFeedback, + overview: { + excellentItems: evaluationWithDetails.excellent_items || 0, + improvementItems: evaluationWithDetails.improvement_items || 0, + overallPerformance: Number(evaluationWithDetails.overall_score) || 0 + }, + improvementSuggestions: { + overallSuggestions: feedbackByType.overall[0]?.content || '無詳細分析', + maintainStrengths: feedbackByType.strength + .filter(f => f.criteria_item_id === null) + .map(f => { + const content = f.content || '無描述'; + const colonIndex = content.indexOf(':'); + const title = colonIndex > -1 ? content.substring(0, colonIndex + 1) : '系統優勢:'; + const description = colonIndex > -1 ? content.substring(colonIndex + 1).trim() : content; + + return { + title: title, + description: description + }; + }), + keyImprovements: (() => { + const allImprovementData = feedbackByType.improvement + .filter(f => f.criteria_item_id === null); + + const improvementData = allImprovementData.length >= 5 + ? allImprovementData.slice(1, -3) + : allImprovementData; + + const tempGroups = []; + let currentGroup = null; + + for (const item of improvementData) { + const content = item.content || ''; + const colonIndex = content.indexOf(':'); + + if (colonIndex > -1) { + const title = content.substring(0, colonIndex + 1); + const remainingContent = content.substring(colonIndex + 1).trim(); + + const suggestionIndex = remainingContent.indexOf('建議:'); + + if (suggestionIndex > -1) { + if (currentGroup) { + tempGroups.push(currentGroup); + } + + currentGroup = { + title: title, + subtitle: '建議:', + suggestions: [] + }; + + const suggestionsText = remainingContent.substring(suggestionIndex + 3).trim(); + const suggestions = suggestionsText.split('\n') + .map(s => s.trim()) + .filter(s => s.startsWith('•')) + .map(s => s.substring(1).trim()); + + currentGroup.suggestions = suggestions; + } else if (remainingContent.startsWith('•')) { + if (currentGroup) { + const suggestion = remainingContent.substring(1).trim(); + currentGroup.suggestions.push(suggestion); + } + } else { + if (currentGroup) { + tempGroups.push(currentGroup); + } + + currentGroup = { + title: title, + subtitle: '', + suggestions: [remainingContent] + }; + } + } else { + if (currentGroup) { + tempGroups.push(currentGroup); + } + + if (content.startsWith('•')) { + if (currentGroup) { + const suggestion = content.substring(1).trim(); + currentGroup.suggestions.push(suggestion); + } else { + currentGroup = { + title: '改進方向:', + subtitle: '', + suggestions: [content.substring(1).trim()] + }; + } + } else { + currentGroup = { + title: '改進方向:', + subtitle: '', + description: content, + suggestions: [] + }; + } + } + } + + if (currentGroup) { + tempGroups.push(currentGroup); + } + + return tempGroups; + })(), + actionPlan: feedbackByType.improvement + .filter(f => f.criteria_item_id === null) + .slice(-3) + .map((f, index) => { + const content = f.content || '無描述'; + const colonIndex = content.indexOf(':'); + const phase = colonIndex > -1 ? content.substring(0, colonIndex + 1) : `階段 ${index + 1}:`; + const description = colonIndex > -1 ? content.substring(colonIndex + 1).trim() : content; + + return { + phase: phase, + description: description + }; + }) + } + }; + + console.log(`📋 開始生成 PDF 報告: 專案=${pdfData.projectTitle}`); + + // 生成 PDF + const pdfBlob = await generateHTMLPDFReport(pdfData); + + console.log(`✅ PDF 報告生成完成: 大小=${pdfBlob.size} bytes`); + + // 返回 PDF 文件 + const fileName = `評審報告_${pdfData.projectTitle}_${pdfData.analysisDate}.pdf`; + + return new NextResponse(pdfBlob, { + status: 200, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`, + 'Content-Length': pdfBlob.size.toString(), + }, + }); + + } catch (error) { + console.error('❌ 生成 PDF 報告失敗:', error); + return NextResponse.json( + { success: false, error: '生成 PDF 報告失敗' }, + { status: 500 } + ); + } +} diff --git a/app/client-layout.tsx b/app/client-layout.tsx index 1e75b21..576973e 100644 --- a/app/client-layout.tsx +++ b/app/client-layout.tsx @@ -5,6 +5,7 @@ import type React from "react" import { Analytics } from "@vercel/analytics/next" import { useSearchParams } from "next/navigation" import { Suspense } from "react" +import { Toaster } from "@/components/ui/toaster" export default function ClientLayout({ children, @@ -16,6 +17,7 @@ export default function ClientLayout({ return ( <> Loading...}>{children} + ) diff --git a/app/history/page.tsx b/app/history/page.tsx index 4615a5e..ea52277 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -7,8 +7,10 @@ import { Card, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog" import { FileText, Calendar, Search, Eye, Download, Trash2, Loader2 } from "lucide-react" import Link from "next/link" +import { useToast } from "@/hooks/use-toast" // 歷史記錄數據類型 interface HistoryItem { @@ -46,6 +48,8 @@ export default function HistoryPage() { }) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [deletingId, setDeletingId] = useState(null) + const { toast } = useToast() // 載入歷史記錄數據 useEffect(() => { @@ -111,6 +115,104 @@ export default function HistoryPage() { } }; + // 下載評審報告 + const handleDownload = async (evaluationId: number, projectTitle: string) => { + try { + toast({ + title: "報告下載中", + description: "評審報告 PDF 正在生成,請稍候...", + }); + + // 調用下載 API + const response = await fetch(`/api/evaluation/${evaluationId}/download`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || '下載失敗'); + } + + // 獲取 PDF Blob + const pdfBlob = await response.blob(); + + // 創建下載連結 + const url = window.URL.createObjectURL(pdfBlob); + const link = document.createElement('a'); + link.href = url; + + // 從 Content-Disposition 標頭獲取檔案名稱,或使用預設名稱 + const contentDisposition = response.headers.get('Content-Disposition'); + let fileName = `評審報告_${projectTitle}_${new Date().toISOString().split('T')[0]}.pdf`; + + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename="(.+)"/); + if (fileNameMatch) { + fileName = decodeURIComponent(fileNameMatch[1]); + } + } + + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + // 顯示成功提示 + toast({ + title: "下載完成", + description: "評審報告已成功下載", + }); + + } catch (error) { + console.error('下載失敗:', error); + toast({ + title: "下載失敗", + description: error instanceof Error ? error.message : '下載過程中發生錯誤', + variant: "destructive", + }); + } + }; + + // 刪除評審報告 + const handleDelete = async (evaluationId: number, projectTitle: string) => { + try { + setDeletingId(evaluationId.toString()); + + const response = await fetch(`/api/evaluation/${evaluationId}/delete`, { + method: 'DELETE', + }); + + const result = await response.json(); + + if (result.success) { + // 從本地狀態中移除已刪除的項目 + setHistoryData(prev => prev.filter(item => item.evaluation_id !== evaluationId)); + + // 重新載入統計數據 + const statsResponse = await fetch('/api/history/stats'); + const statsResult = await statsResponse.json(); + if (statsResult.success) { + setStatsData(statsResult.data); + } + + toast({ + title: "刪除成功", + description: `評審報告「${projectTitle}」已成功刪除`, + }); + } else { + throw new Error(result.error || '刪除失敗'); + } + } catch (error) { + console.error('刪除失敗:', error); + toast({ + title: "刪除失敗", + description: error instanceof Error ? error.message : '刪除過程中發生錯誤', + variant: "destructive", + }); + } finally { + setDeletingId(null); + } + }; + return (
@@ -264,7 +366,11 @@ export default function HistoryPage() { - @@ -273,9 +379,48 @@ export default function HistoryPage() { 處理中... )} - + + + + + + + 確認刪除 + + 您確定要刪除評審報告「{item.title}」嗎?此操作將永久刪除: +
+ • 評審結果和分數 +
+ • 專案文件和上傳的檔案 +
+ • 所有相關的評語和建議 +
+
+ 此操作無法復原! +
+
+ + 取消 + item.evaluation_id && handleDelete(item.evaluation_id, item.title)} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + 確認刪除 + + +
+
diff --git a/app/results/page.tsx b/app/results/page.tsx index 2f976c0..6889a06 100644 --- a/app/results/page.tsx +++ b/app/results/page.tsx @@ -27,6 +27,7 @@ import { } from "recharts" import { Download, Share2, TrendingUp, AlertCircle, CheckCircle, Star } from "lucide-react" import { useToast } from "@/hooks/use-toast" +import { ShareModal } from "@/components/share-modal" // 模擬評分結果數據 - 使用您提到的例子:8, 7, 6, 8, 4,平均 = 6.6 const mockCriteria = [ @@ -129,7 +130,8 @@ export default function ResultsPage() { const [activeTab, setActiveTab] = useState("overview") const [evaluationData, setEvaluationData] = useState(null) const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) + const [error, setError] = useState(null) + const [isShareModalOpen, setIsShareModalOpen] = useState(false) const { toast } = useToast() const searchParams = useSearchParams() @@ -168,10 +170,10 @@ export default function ResultsPage() { } } catch (error) { console.error('載入評審結果失敗:', error) - setError(error.message) + setError((error as Error).message) toast({ title: "載入失敗", - description: error.message || "無法載入評審結果,請重新進行評審", + description: (error as Error).message || "無法載入評審結果,請重新進行評審", variant: "destructive", }) } finally { @@ -230,10 +232,10 @@ export default function ResultsPage() { } // 使用真實數據或回退到模擬數據 - const results = evaluationData.evaluation?.fullData || evaluationData || mockResults + const results = (evaluationData as any)?.evaluation?.fullData || evaluationData || mockResults // 計算統計數據 - 基於 criteria_items 的平均分作為閾值 - const calculateOverview = (criteria: any[]) => { + const calculateOverview = (criteria: any[]): any => { if (!criteria || criteria.length === 0) { return { excellentItems: 0, @@ -278,18 +280,72 @@ export default function ResultsPage() { } } - const downloadReport = () => { - toast({ - title: "報告下載中", - description: "評審報告 PDF 正在生成,請稍候...", - }) + const downloadReport = async () => { + try { + // 顯示載入提示 + toast({ + title: "報告下載中", + description: "評審報告 PDF 正在生成,請稍候...", + }); + + // 獲取評審 ID + const evaluationId = searchParams.get('id'); + + if (!evaluationId) { + throw new Error('無法獲取評審 ID'); + } + + // 調用下載 API + const response = await fetch(`/api/evaluation/${evaluationId}/download`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || '下載失敗'); + } + + // 獲取 PDF Blob + const pdfBlob = await response.blob(); + + // 創建下載連結 + const url = window.URL.createObjectURL(pdfBlob); + const link = document.createElement('a'); + link.href = url; + + // 從 Content-Disposition 標頭獲取檔案名稱,或使用預設名稱 + const contentDisposition = response.headers.get('Content-Disposition'); + let fileName = `評審報告_${safeResults.projectTitle}_${safeResults.analysisDate}.pdf`; + + if (contentDisposition) { + const fileNameMatch = contentDisposition.match(/filename="(.+)"/); + if (fileNameMatch) { + fileName = decodeURIComponent(fileNameMatch[1]); + } + } + + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + // 顯示成功提示 + toast({ + title: "下載完成", + description: "評審報告已成功下載", + }); + + } catch (error) { + console.error('下載失敗:', error); + toast({ + title: "下載失敗", + description: error instanceof Error ? error.message : '下載過程中發生錯誤', + variant: "destructive", + }); + } } const shareResults = () => { - toast({ - title: "分享連結已複製", - description: "評審結果分享連結已複製到剪貼板", - }) + setIsShareModalOpen(true) } const getScoreColor = (score: number, maxScore: number) => { @@ -384,7 +440,7 @@ export default function ResultsPage() {
- {safeResults.criteria.map((item, index) => ( + {safeResults.criteria.map((item: any, index: number) => (
@@ -432,7 +488,7 @@ export default function ResultsPage() { - {safeResults.criteria.map((item, index) => ( + {safeResults.criteria.map((item: any, index: number) => (
@@ -455,7 +511,7 @@ export default function ResultsPage() {

優點

    - {item.strengths.map((strength, i) => ( + {item.strengths.map((strength: any, i: number) => (
  • {strength} @@ -466,7 +522,7 @@ export default function ResultsPage() {

    改進建議

      - {item.improvements.map((improvement, i) => ( + {item.improvements.map((improvement: any, i: number) => (
    • {improvement} @@ -518,7 +574,7 @@ export default function ResultsPage() { fill="#8884d8" dataKey="value" > - {safeResults.chartData.pieChart.map((entry, index) => ( + {safeResults.chartData.pieChart.map((entry: any, index: number) => ( ))} @@ -558,7 +614,7 @@ export default function ResultsPage() {

      繼續保持的優勢

      - {safeResults.improvementSuggestions.maintainStrengths.map((strength, index) => ( + {safeResults.improvementSuggestions.maintainStrengths.map((strength: any, index: number) => (
      {strength.title}

      @@ -572,12 +628,12 @@ export default function ResultsPage() {

      重點改進方向

      - {safeResults.improvementSuggestions.keyImprovements.map((improvement, index) => ( + {safeResults.improvementSuggestions.keyImprovements.map((improvement: any, index: number) => (
      {improvement.title}

      {improvement.description}

        - {improvement.suggestions.map((suggestion, sIndex) => ( + {improvement.suggestions.map((suggestion: any, sIndex: number) => (
      • • {suggestion}
      • ))}
      @@ -589,7 +645,7 @@ export default function ResultsPage() {

      下一步行動計劃

      - {safeResults.improvementSuggestions.actionPlan.map((action, index) => ( + {safeResults.improvementSuggestions.actionPlan.map((action: any, index: number) => (
      {index + 1} @@ -608,6 +664,14 @@ export default function ResultsPage() {
      + + {/* Share Modal */} + setIsShareModalOpen(false)} + evaluationId={searchParams.get('id') || undefined} + projectTitle={safeResults.projectTitle} + />
      ) } diff --git a/components/share-modal.tsx b/components/share-modal.tsx new file mode 100644 index 0000000..d77bccf --- /dev/null +++ b/components/share-modal.tsx @@ -0,0 +1,296 @@ +"use client" + +import { useState, useEffect } from "react" +import { Copy, Check, Share2, QrCode, Mail } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { useToast } from "@/hooks/use-toast" +import { generateShareUrl, getCurrentUrl } from "@/lib/config" +import QRCode from "qrcode" + +interface ShareModalProps { + isOpen: boolean + onClose: () => void + evaluationId?: string + projectTitle?: string +} + +export function ShareModal({ isOpen, onClose, evaluationId, projectTitle }: ShareModalProps) { + const [qrCodeUrl, setQrCodeUrl] = useState("") + const [isGeneratingQR, setIsGeneratingQR] = useState(false) + const [copiedLink, setCopiedLink] = useState(false) + const [copiedQR, setCopiedQR] = useState(false) + const { toast } = useToast() + + // 生成分享連結 + const shareUrl = evaluationId + ? generateShareUrl(evaluationId) + : getCurrentUrl() + + // 生成 QR Code + useEffect(() => { + if (isOpen && shareUrl) { + setIsGeneratingQR(true) + QRCode.toDataURL(shareUrl, { + width: 160, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + } + }) + .then(url => { + setQrCodeUrl(url) + setIsGeneratingQR(false) + }) + .catch(err => { + console.error('生成 QR Code 失敗:', err) + setIsGeneratingQR(false) + toast({ + title: "錯誤", + description: "生成 QR Code 失敗,請稍後再試", + variant: "destructive", + }) + }) + } + }, [isOpen, shareUrl, toast]) + + // 複製連結 + const copyLink = async () => { + try { + await navigator.clipboard.writeText(shareUrl) + setCopiedLink(true) + toast({ + title: "成功", + description: "連結已複製到剪貼板", + }) + setTimeout(() => setCopiedLink(false), 2000) + } catch (err) { + console.error('複製失敗:', err) + toast({ + title: "錯誤", + description: "複製失敗,請手動複製連結", + variant: "destructive", + }) + } + } + + // 複製 QR Code + const copyQRCode = async () => { + if (!qrCodeUrl) return + + try { + // 將 base64 轉換為 blob + const response = await fetch(qrCodeUrl) + const blob = await response.blob() + + // 複製到剪貼板 + await navigator.clipboard.write([ + new ClipboardItem({ + [blob.type]: blob + }) + ]) + + setCopiedQR(true) + toast({ + title: "成功", + description: "QR Code 已複製到剪貼板", + }) + setTimeout(() => setCopiedQR(false), 2000) + } catch (err) { + console.error('複製 QR Code 失敗:', err) + toast({ + title: "錯誤", + description: "複製 QR Code 失敗,請截圖保存", + variant: "destructive", + }) + } + } + + // 分享到社群媒體 + const shareToSocial = (platform: string) => { + const encodedUrl = encodeURIComponent(shareUrl) + const encodedTitle = encodeURIComponent(`評審結果 - ${projectTitle || 'AI 智能評審系統'}`) + + let shareUrl_template = "" + + switch (platform) { + case 'line': + shareUrl_template = `https://social-plugins.line.me/lineit/share?url=${encodedUrl}&text=${encodedTitle}` + break + case 'facebook': + shareUrl_template = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}` + break + case 'email': + const emailBody = encodeURIComponent(`您好,\n\n我想與您分享這個評審結果:\n\n${projectTitle || 'AI 智能評審系統'}\n\n請點擊以下連結查看詳細報告:\n${shareUrl}\n\n感謝!`) + shareUrl_template = `mailto:?subject=${encodedTitle}&body=${emailBody}` + break + default: + return + } + + if (platform === 'email') { + window.location.href = shareUrl_template + } else { + window.open(shareUrl_template, '_blank', 'width=600,height=400') + } + } + + return ( + + + + + + 分享評審報告 + + + 分享此評審結果給其他人查看 + + + +
      + {/* 分享連結 */} + + + 分享連結 + + 複製此連結分享給其他人 + + + +
      +
      + {shareUrl} +
      + +
      +
      +
      + + {/* QR Code */} + + + + + QR Code + + + 掃描 QR Code 快速訪問報告 + + + +
      + {isGeneratingQR ? ( +
      +
      +
      + ) : qrCodeUrl ? ( +
      + QR Code + +
      + ) : ( +
      + 生成中... +
      + )} +
      +
      +
      + + {/* 社群分享 */} + + + 社群分享 + + 直接分享到社群平台 + + + +
      + + + +
      +
      +
      +
      +
      +
      + ) +} diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..25e7b47 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
      +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
      +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..89c8ac7 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + 關閉 + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
      +) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
      +) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx new file mode 100644 index 0000000..f6adb95 --- /dev/null +++ b/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/hooks/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
      + {title && {title}} + {description && ( + {description} + )} +
      + {action} + +
      + ) + })} + +
      + ) +} diff --git a/env.example b/env.example new file mode 100644 index 0000000..047ce7f --- /dev/null +++ b/env.example @@ -0,0 +1,15 @@ +# 應用配置 +NEXT_PUBLIC_APP_URL=http://localhost:12024 +NEXT_PUBLIC_APP_NAME=AI 智能評審系統 + +# 資料庫配置 +DB_HOST=mysql.theaken.com +DB_PORT=33306 +DB_NAME=db_AI_scoring +DB_USER=root +DB_PASSWORD=zh6161168 + +# AI 配置 +GEMINI_API_KEY=AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4 +GEMINI_MODEL=gemini-1.5-pro +GEMINI_MAX_TOKENS=8192 diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..c1d6c22 --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,89 @@ +/** + * 應用配置工具類 + * 用於管理環境變量和應用設置 + */ + +// 資料庫配置 +export const dbConfig = { + host: process.env.DB_HOST || 'mysql.theaken.com', + port: parseInt(process.env.DB_PORT || '33306'), + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'zh6161168', + database: process.env.DB_NAME || 'db_AI_scoring', + charset: 'utf8mb4', + timezone: '+08:00', + acquireTimeout: 60000, + timeout: 60000, + reconnect: true, + multipleStatements: true, +} as const + +// AI 配置 +export const aiConfig = { + geminiApiKey: process.env.GEMINI_API_KEY || 'AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4', + modelName: process.env.GEMINI_MODEL || 'gemini-1.5-pro', + maxTokens: parseInt(process.env.GEMINI_MAX_TOKENS || '8192'), +} as const + +// 獲取應用基礎 URL +export function getAppUrl(): string { + // 在客戶端使用 window.location.origin + if (typeof window !== 'undefined') { + return window.location.origin + } + + // 在服務端使用環境變量,如果沒有設置則使用 localhost:12024 + return process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:12024' +} + +// 獲取應用名稱 +export function getAppName(): string { + return process.env.NEXT_PUBLIC_APP_NAME || 'AI 智能評審系統' +} + +// 生成分享連結 +export function generateShareUrl(evaluationId: string): string { + const baseUrl = getAppUrl() + return `${baseUrl}/results?id=${evaluationId}` +} + +// 生成當前頁面連結 +export function getCurrentUrl(): string { + if (typeof window !== 'undefined') { + return window.location.href + } + return getAppUrl() +} + +// 環境變量配置 +export const config = { + appUrl: getAppUrl(), + appName: getAppName(), + isDevelopment: process.env.NODE_ENV === 'development', + isProduction: process.env.NODE_ENV === 'production', + database: dbConfig, + ai: aiConfig, +} as const + +// 配置驗證 +export function validateConfig(): { isValid: boolean; errors: string[] } { + const errors: string[] = [] + + // 檢查必要的環境變量 + if (!process.env.GEMINI_API_KEY) { + errors.push('GEMINI_API_KEY 環境變量未設置') + } + + if (!process.env.DB_HOST) { + errors.push('DB_HOST 環境變量未設置') + } + + if (!process.env.DB_NAME) { + errors.push('DB_NAME 環境變量未設置') + } + + return { + isValid: errors.length === 0, + errors + } +} diff --git a/lib/database.ts b/lib/database.ts index 8c0df39..f036ff4 100644 --- a/lib/database.ts +++ b/lib/database.ts @@ -1,19 +1,5 @@ import mysql from 'mysql2/promise'; - -// 資料庫配置 -const dbConfig = { - host: process.env.DB_HOST || 'mysql.theaken.com', - port: parseInt(process.env.DB_PORT || '33306'), - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || 'zh6161168', - database: process.env.DB_NAME || 'db_AI_scoring', - charset: 'utf8mb4', - timezone: '+08:00', - acquireTimeout: 60000, - timeout: 60000, - reconnect: true, - multipleStatements: true, -}; +import { dbConfig } from './config'; // 建立連接池 const pool = mysql.createPool({ diff --git a/lib/services/gemini.ts b/lib/services/gemini.ts index d12d240..587a371 100644 --- a/lib/services/gemini.ts +++ b/lib/services/gemini.ts @@ -1,7 +1,7 @@ import { GoogleGenerativeAI } from '@google/generative-ai'; +import { aiConfig } from '../config'; -const API_KEY = 'AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4'; -const genAI = new GoogleGenerativeAI(API_KEY); +const genAI = new GoogleGenerativeAI(aiConfig.geminiApiKey); export interface CriteriaItem { id: string; diff --git a/lib/utils/html-pdf-generator.ts b/lib/utils/html-pdf-generator.ts new file mode 100644 index 0000000..6c291ec --- /dev/null +++ b/lib/utils/html-pdf-generator.ts @@ -0,0 +1,504 @@ +import puppeteer from 'puppeteer'; + +export interface PDFReportData { + projectTitle: string; + overallScore: number; + totalPossible: number; + grade: string; + analysisDate: string; + criteria: Array<{ + name: string; + score: number; + maxScore: number; + weight: number; + weightedScore: number; + percentage: number; + feedback: string; + strengths: string[]; + improvements: string[]; + }>; + overview: { + excellentItems: number; + improvementItems: number; + overallPerformance: number; + }; + improvementSuggestions: { + overallSuggestions: string; + maintainStrengths: Array<{ + title: string; + description: string; + }>; + keyImprovements: Array<{ + title: string; + subtitle?: string; + description?: string; + suggestions: string[]; + }>; + actionPlan: Array<{ + phase: string; + description: string; + }>; + }; +} + +export class HTMLPDFGenerator { + private generateHTML(data: PDFReportData): string { + return ` + + + + + + AI 智能評審報告 + + + +
      +

      AI 智能評審報告

      +

      專案名稱:${data.projectTitle}

      +

      分析日期:${data.analysisDate}

      +
      + +
      +

      總評結果

      +
      +
      ${data.overallScore}/${data.totalPossible}
      +
      ${data.grade}
      +
      +
      + +
      +

      統計概覽

      +
      +
      +
      ${data.overview.excellentItems}
      +
      優秀項目
      +
      +
      +
      ${data.overview.improvementItems}
      +
      待改進項目
      +
      +
      +
      ${data.overview.overallPerformance}%
      +
      整體表現
      +
      +
      +
      + +
      +

      評分明細

      + + + + + + + + + + + ${data.criteria.map(item => ` + + + + + + + `).join('')} + +
      評分項目得分權重加權分
      ${item.name}${item.score}/${item.maxScore}${item.weight}%${item.weightedScore.toFixed(1)}
      +
      + +
      +

      詳細分析

      + ${data.criteria.map(item => ` +
      +
      ${item.name}
      +
      得分:${item.score}/${item.maxScore} (${item.percentage.toFixed(1)}%)
      +
      + AI 評語:${item.feedback} +
      + ${item.strengths.length > 0 ? ` +
      +

      優點:

      +
        + ${item.strengths.map(strength => `
      • ${strength}
      • `).join('')} +
      +
      + ` : ''} + ${item.improvements.length > 0 ? ` +
      +

      改進建議:

      +
        + ${item.improvements.map(improvement => `
      • ${improvement}
      • `).join('')} +
      +
      + ` : ''} +
      + `).join('')} +
      + +
      +

      整體改進建議

      +
      +

      ${data.improvementSuggestions.overallSuggestions}

      +
      +
      + + ${data.improvementSuggestions.maintainStrengths.length > 0 ? ` +
      +

      繼續保持的優勢

      +
      + ${data.improvementSuggestions.maintainStrengths.map(strength => ` +
      +
      ${strength.title}
      +
      ${strength.description}
      +
      + `).join('')} +
      +
      + ` : ''} + + ${data.improvementSuggestions.keyImprovements.length > 0 ? ` +
      +

      重點改進方向

      +
      + ${data.improvementSuggestions.keyImprovements.map(improvement => ` +
      +
      ${improvement.title}
      + ${improvement.description ? `
      ${improvement.description}
      ` : ''} + ${improvement.suggestions.length > 0 ? ` +
      +
        + ${improvement.suggestions.map(suggestion => `
      • ${suggestion}
      • `).join('')} +
      +
      + ` : ''} +
      + `).join('')} +
      +
      + ` : ''} + + ${data.improvementSuggestions.actionPlan.length > 0 ? ` +
      +

      下一步行動計劃

      +
      + ${data.improvementSuggestions.actionPlan.map((action, index) => ` +
      +
      ${index + 1}
      +
      +
      ${action.phase}
      +
      ${action.description}
      +
      +
      + `).join('')} +
      +
      + ` : ''} + + + + + `; + } + + public async generateReport(data: PDFReportData): Promise { + const browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--no-first-run', + '--no-zygote', + '--disable-gpu' + ], + timeout: 30000 + }); + + try { + const page = await browser.newPage(); + + // 設置頁面內容 + const html = this.generateHTML(data); + await page.setContent(html, { waitUntil: 'networkidle0' }); + + // 生成 PDF + const pdfBuffer = await page.pdf({ + format: 'A4', + printBackground: true, + margin: { + top: '20mm', + right: '20mm', + bottom: '20mm', + left: '20mm' + } + }); + + return new Blob([pdfBuffer], { type: 'application/pdf' }); + } finally { + await browser.close(); + } + } +} + +// 便捷函數 +export async function generateHTMLPDFReport(data: PDFReportData): Promise { + const generator = new HTMLPDFGenerator(); + return generator.generateReport(data); +} diff --git a/lib/utils/pdf-generator-chinese.ts b/lib/utils/pdf-generator-chinese.ts new file mode 100644 index 0000000..cfa79c7 --- /dev/null +++ b/lib/utils/pdf-generator-chinese.ts @@ -0,0 +1,319 @@ +import jsPDF from 'jspdf'; +import 'jspdf-autotable'; + +export interface PDFReportData { + projectTitle: string; + overallScore: number; + totalPossible: number; + grade: string; + analysisDate: string; + criteria: Array<{ + name: string; + score: number; + maxScore: number; + weight: number; + weightedScore: number; + percentage: number; + feedback: string; + strengths: string[]; + improvements: string[]; + }>; + overview: { + excellentItems: number; + improvementItems: number; + overallPerformance: number; + }; + improvementSuggestions: { + overallSuggestions: string; + maintainStrengths: Array<{ + title: string; + description: string; + }>; + keyImprovements: Array<{ + title: string; + subtitle?: string; + description?: string; + suggestions: string[]; + }>; + actionPlan: Array<{ + phase: string; + description: string; + }>; + }; +} + +export class ChinesePDFReportGenerator { + private doc: jsPDF; + private currentY: number = 20; + private pageHeight: number = 280; + private margin: number = 20; + + constructor() { + this.doc = new jsPDF('p', 'mm', 'a4'); + } + + private addNewPageIfNeeded(requiredSpace: number = 20): void { + if (this.currentY + requiredSpace > this.pageHeight) { + this.doc.addPage(); + this.currentY = 20; + } + } + + private addTitle(text: string, fontSize: number = 16, isBold: boolean = true): void { + this.addNewPageIfNeeded(10); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', isBold ? 'bold' : 'normal'); + + // 將中文轉換為可顯示的格式 + const displayText = this.convertChineseText(text); + this.doc.text(displayText, this.margin, this.currentY); + this.currentY += fontSize + 5; + } + + private addSubtitle(text: string, fontSize: number = 12): void { + this.addNewPageIfNeeded(8); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', 'bold'); + + const displayText = this.convertChineseText(text); + this.doc.text(displayText, this.margin, this.currentY); + this.currentY += fontSize + 3; + } + + private addText(text: string, fontSize: number = 10, isBold: boolean = false): void { + this.addNewPageIfNeeded(6); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', isBold ? 'bold' : 'normal'); + + const displayText = this.convertChineseText(text); + + // 處理長文本換行 + const maxWidth = 170; + const lines = this.doc.splitTextToSize(displayText, maxWidth); + + for (const line of lines) { + this.addNewPageIfNeeded(6); + this.doc.text(line, this.margin, this.currentY); + this.currentY += 6; + } + } + + private addBulletList(items: string[], fontSize: number = 10): void { + for (const item of items) { + this.addNewPageIfNeeded(6); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', 'normal'); + + const displayText = this.convertChineseText(item); + this.doc.text(`• ${displayText}`, this.margin + 5, this.currentY); + this.currentY += 6; + } + } + + private addScoreBox(score: number, total: number, grade: string): void { + this.addNewPageIfNeeded(25); + + // 繪製分數框 + this.doc.setFillColor(240, 240, 240); + this.doc.rect(this.margin, this.currentY, 50, 20, 'F'); + + // 分數文字 + this.doc.setFontSize(18); + this.doc.setFont('helvetica', 'bold'); + this.doc.text(`${score}/${total}`, this.margin + 5, this.currentY + 12); + + // 等級 + this.doc.setFontSize(12); + this.doc.text(grade, this.margin + 5, this.currentY + 18); + + this.currentY += 25; + } + + private addCriteriaTable(criteria: PDFReportData['criteria']): void { + this.addNewPageIfNeeded(30); + + // 準備表格數據 + const tableData = criteria.map(item => [ + this.convertChineseText(item.name), + `${item.score}/${item.maxScore}`, + `${item.weight}%`, + item.weightedScore.toFixed(1) + ]); + + // 使用 autoTable 插件創建表格 + (this.doc as any).autoTable({ + head: [['評分項目', '得分', '權重', '加權分']], + body: tableData, + startY: this.currentY, + margin: { left: this.margin, right: this.margin }, + styles: { fontSize: 10 }, + headStyles: { fillColor: [66, 139, 202] }, + alternateRowStyles: { fillColor: [245, 245, 245] } + }); + + // 更新當前 Y 位置 + this.currentY = (this.doc as any).lastAutoTable.finalY + 10; + } + + // 將中文字符轉換為可顯示的格式 + private convertChineseText(text: string): string { + // 簡單的字符替換,將常見的中文字符替換為英文描述 + const chineseMap: { [key: string]: string } = { + '評審結果': 'Evaluation Results', + 'AI 智能評審報告': 'AI Intelligent Evaluation Report', + '專案名稱': 'Project Name', + '分析日期': 'Analysis Date', + '總評結果': 'Overall Results', + '統計概覽': 'Statistical Overview', + '優秀項目': 'Excellent Items', + '待改進項目': 'Items to Improve', + '整體表現': 'Overall Performance', + '評分明細': 'Score Details', + '評分項目': 'Evaluation Items', + '得分': 'Score', + '權重': 'Weight', + '加權分': 'Weighted Score', + '詳細分析': 'Detailed Analysis', + 'AI 評語': 'AI Comments', + '優點': 'Strengths', + '改進建議': 'Improvement Suggestions', + '整體改進建議': 'Overall Improvement Suggestions', + '繼續保持的優勢': 'Maintain Strengths', + '重點改進方向': 'Key Improvement Areas', + '下一步行動計劃': 'Next Action Plan', + '本報告由 AI 智能評審系統生成': 'Generated by AI Intelligent Evaluation System', + '生成時間': 'Generated Time', + '內容品質': 'Content Quality', + '視覺設計': 'Visual Design', + '邏輯結構': 'Logical Structure', + '創新性': 'Innovation', + '實用性': 'Practicality' + }; + + let result = text; + + // 替換已知的中文詞彙 + for (const [chinese, english] of Object.entries(chineseMap)) { + result = result.replace(new RegExp(chinese, 'g'), english); + } + + // 對於其他中文字符,使用 Unicode 轉換 + result = result.replace(/[\u4e00-\u9fff]/g, (char) => { + const code = char.charCodeAt(0); + return `[U+${code.toString(16).toUpperCase()}]`; + }); + + return result; + } + + public generateReport(data: PDFReportData): Promise { + return new Promise((resolve, reject) => { + try { + // 標題頁 + this.addTitle('AI Intelligent Evaluation Report', 20); + this.addText(`Project Name: ${data.projectTitle}`, 12, true); + this.addText(`Analysis Date: ${data.analysisDate}`, 12); + this.currentY += 10; + + // 總分顯示 + this.addSubtitle('Overall Results'); + this.addScoreBox(data.overallScore, data.totalPossible, data.grade); + + // 統計概覽 + this.addSubtitle('Statistical Overview'); + this.addText(`Excellent Items: ${data.overview.excellentItems} items`); + this.addText(`Items to Improve: ${data.overview.improvementItems} items`); + this.addText(`Overall Performance: ${data.overview.overallPerformance}%`); + this.currentY += 10; + + // 評分明細 + this.addSubtitle('Score Details'); + this.addCriteriaTable(data.criteria); + + // 詳細分析 + this.addSubtitle('Detailed Analysis'); + for (const item of data.criteria) { + this.addNewPageIfNeeded(20); + this.doc.setFontSize(11); + this.doc.setFont('helvetica', 'bold'); + this.doc.text(this.convertChineseText(item.name), this.margin, this.currentY); + this.currentY += 8; + + this.addText(`Score: ${item.score}/${item.maxScore} (${item.percentage.toFixed(1)}%)`, 10); + this.addText(`AI Comments: ${item.feedback}`, 10); + + if (item.strengths.length > 0) { + this.addText('Strengths:', 10, true); + this.addBulletList(item.strengths, 9); + } + + if (item.improvements.length > 0) { + this.addText('Improvement Suggestions:', 10, true); + this.addBulletList(item.improvements, 9); + } + + this.currentY += 10; + } + + // 改進建議 + this.addSubtitle('Overall Improvement Suggestions'); + this.addText(data.improvementSuggestions.overallSuggestions, 10); + this.currentY += 10; + + // 保持的優勢 + if (data.improvementSuggestions.maintainStrengths.length > 0) { + this.addSubtitle('Maintain Strengths'); + for (const strength of data.improvementSuggestions.maintainStrengths) { + this.addText(strength.title, 10, true); + this.addText(strength.description, 9); + this.currentY += 5; + } + } + + // 重點改進方向 + if (data.improvementSuggestions.keyImprovements.length > 0) { + this.addSubtitle('Key Improvement Areas'); + for (const improvement of data.improvementSuggestions.keyImprovements) { + this.addText(improvement.title, 10, true); + if (improvement.description) { + this.addText(improvement.description, 9); + } + if (improvement.suggestions.length > 0) { + this.addBulletList(improvement.suggestions, 9); + } + this.currentY += 5; + } + } + + // 行動計劃 + if (data.improvementSuggestions.actionPlan.length > 0) { + this.addSubtitle('Next Action Plan'); + for (let i = 0; i < data.improvementSuggestions.actionPlan.length; i++) { + const action = data.improvementSuggestions.actionPlan[i]; + this.addText(`${i + 1}. ${action.phase}`, 10, true); + this.addText(action.description, 9); + this.currentY += 5; + } + } + + // 頁腳 + this.doc.setFontSize(8); + this.doc.setFont('helvetica', 'normal'); + this.doc.text('Generated by AI Intelligent Evaluation System', this.margin, this.pageHeight - 10); + this.doc.text(`Generated Time: ${new Date().toLocaleString('en-US')}`, this.margin + 100, this.pageHeight - 10); + + // 生成 PDF Blob + const pdfBlob = this.doc.output('blob'); + resolve(pdfBlob); + } catch (error) { + reject(error); + } + }); + } +} + +// 便捷函數 +export async function generateChinesePDFReport(data: PDFReportData): Promise { + const generator = new ChinesePDFReportGenerator(); + return generator.generateReport(data); +} diff --git a/lib/utils/pdf-generator.ts b/lib/utils/pdf-generator.ts new file mode 100644 index 0000000..8edf738 --- /dev/null +++ b/lib/utils/pdf-generator.ts @@ -0,0 +1,269 @@ +import jsPDF from 'jspdf'; +import 'jspdf-autotable'; +import html2canvas from 'html2canvas'; + +export interface PDFReportData { + projectTitle: string; + overallScore: number; + totalPossible: number; + grade: string; + analysisDate: string; + criteria: Array<{ + name: string; + score: number; + maxScore: number; + weight: number; + weightedScore: number; + percentage: number; + feedback: string; + strengths: string[]; + improvements: string[]; + }>; + overview: { + excellentItems: number; + improvementItems: number; + overallPerformance: number; + }; + improvementSuggestions: { + overallSuggestions: string; + maintainStrengths: Array<{ + title: string; + description: string; + }>; + keyImprovements: Array<{ + title: string; + subtitle?: string; + description?: string; + suggestions: string[]; + }>; + actionPlan: Array<{ + phase: string; + description: string; + }>; + }; +} + +export class PDFReportGenerator { + private doc: jsPDF; + private currentY: number = 20; + private pageHeight: number = 280; + private margin: number = 20; + + constructor() { + this.doc = new jsPDF('p', 'mm', 'a4'); + // 設置支援中文的默認字體 + this.doc.setFont('helvetica'); + } + + private addNewPageIfNeeded(requiredSpace: number = 20): void { + if (this.currentY + requiredSpace > this.pageHeight) { + this.doc.addPage(); + this.currentY = 20; + } + } + + private addTitle(text: string, fontSize: number = 16, isBold: boolean = true): void { + this.addNewPageIfNeeded(10); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', isBold ? 'bold' : 'normal'); + this.doc.text(text, this.margin, this.currentY); + this.currentY += fontSize + 5; + } + + private addSubtitle(text: string, fontSize: number = 12): void { + this.addNewPageIfNeeded(8); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', 'bold'); + this.doc.text(text, this.margin, this.currentY); + this.currentY += fontSize + 3; + } + + private addText(text: string, fontSize: number = 10, isBold: boolean = false): void { + this.addNewPageIfNeeded(6); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', isBold ? 'bold' : 'normal'); + + // 處理長文本換行 + const maxWidth = 170; // A4 寬度減去邊距 + const lines = this.doc.splitTextToSize(text, maxWidth); + + for (const line of lines) { + this.addNewPageIfNeeded(6); + this.doc.text(line, this.margin, this.currentY); + this.currentY += 6; + } + } + + private addBulletList(items: string[], fontSize: number = 10): void { + for (const item of items) { + this.addNewPageIfNeeded(6); + this.doc.setFontSize(fontSize); + this.doc.setFont('helvetica', 'normal'); + this.doc.text(`• ${item}`, this.margin + 5, this.currentY); + this.currentY += 6; + } + } + + private addScoreBox(score: number, total: number, grade: string): void { + this.addNewPageIfNeeded(25); + + // 繪製分數框 + this.doc.setFillColor(240, 240, 240); + this.doc.rect(this.margin, this.currentY, 50, 20, 'F'); + + // 分數文字 + this.doc.setFontSize(18); + this.doc.setFont('helvetica', 'bold'); + this.doc.text(`${score}/${total}`, this.margin + 5, this.currentY + 12); + + // 等級 + this.doc.setFontSize(12); + this.doc.text(grade, this.margin + 5, this.currentY + 18); + + this.currentY += 25; + } + + private addCriteriaTable(criteria: PDFReportData['criteria']): void { + this.addNewPageIfNeeded(30); + + // 表頭 + this.doc.setFontSize(10); + this.doc.setFont('helvetica', 'bold'); + this.doc.text('評分項目', this.margin, this.currentY); + this.doc.text('得分', this.margin + 100, this.currentY); + this.doc.text('權重', this.margin + 130, this.currentY); + this.doc.text('加權分', this.margin + 150, this.currentY); + + this.currentY += 8; + + // 分隔線 + this.doc.line(this.margin, this.currentY, this.margin + 170, this.currentY); + this.currentY += 5; + + // 數據行 + for (const item of criteria) { + this.addNewPageIfNeeded(15); + + this.doc.setFont('helvetica', 'normal'); + this.doc.text(item.name, this.margin, this.currentY); + this.doc.text(`${item.score}/${item.maxScore}`, this.margin + 100, this.currentY); + this.doc.text(`${item.weight}%`, this.margin + 130, this.currentY); + this.doc.text(item.weightedScore.toFixed(1), this.margin + 150, this.currentY); + + this.currentY += 6; + } + + this.currentY += 10; + } + + public generateReport(data: PDFReportData): Promise { + return new Promise((resolve, reject) => { + try { + // 標題頁 + this.addTitle('AI 智能評審報告', 20); + this.addText(`專案名稱:${data.projectTitle}`, 12, true); + this.addText(`分析日期:${data.analysisDate}`, 12); + this.currentY += 10; + + // 總分顯示 + this.addSubtitle('總評結果'); + this.addScoreBox(data.overallScore, data.totalPossible, data.grade); + + // 統計概覽 + this.addSubtitle('統計概覽'); + this.addText(`優秀項目:${data.overview.excellentItems} 項`); + this.addText(`待改進項目:${data.overview.improvementItems} 項`); + this.addText(`整體表現:${data.overview.overallPerformance}%`); + this.currentY += 10; + + // 評分明細 + this.addSubtitle('評分明細'); + this.addCriteriaTable(data.criteria); + + // 詳細分析 + this.addSubtitle('詳細分析'); + for (const item of data.criteria) { + this.addNewPageIfNeeded(20); + this.doc.setFontSize(11); + this.doc.setFont('helvetica', 'bold'); + this.doc.text(item.name, this.margin, this.currentY); + this.currentY += 8; + + this.addText(`得分:${item.score}/${item.maxScore} (${item.percentage.toFixed(1)}%)`, 10); + this.addText(`AI 評語:${item.feedback}`, 10); + + if (item.strengths.length > 0) { + this.addText('優點:', 10, true); + this.addBulletList(item.strengths, 9); + } + + if (item.improvements.length > 0) { + this.addText('改進建議:', 10, true); + this.addBulletList(item.improvements, 9); + } + + this.currentY += 10; + } + + // 改進建議 + this.addSubtitle('整體改進建議'); + this.addText(data.improvementSuggestions.overallSuggestions, 10); + this.currentY += 10; + + // 保持的優勢 + if (data.improvementSuggestions.maintainStrengths.length > 0) { + this.addSubtitle('繼續保持的優勢'); + for (const strength of data.improvementSuggestions.maintainStrengths) { + this.addText(strength.title, 10, true); + this.addText(strength.description, 9); + this.currentY += 5; + } + } + + // 重點改進方向 + if (data.improvementSuggestions.keyImprovements.length > 0) { + this.addSubtitle('重點改進方向'); + for (const improvement of data.improvementSuggestions.keyImprovements) { + this.addText(improvement.title, 10, true); + if (improvement.description) { + this.addText(improvement.description, 9); + } + if (improvement.suggestions.length > 0) { + this.addBulletList(improvement.suggestions, 9); + } + this.currentY += 5; + } + } + + // 行動計劃 + if (data.improvementSuggestions.actionPlan.length > 0) { + this.addSubtitle('下一步行動計劃'); + for (let i = 0; i < data.improvementSuggestions.actionPlan.length; i++) { + const action = data.improvementSuggestions.actionPlan[i]; + this.addText(`${i + 1}. ${action.phase}`, 10, true); + this.addText(action.description, 9); + this.currentY += 5; + } + } + + // 頁腳 + this.doc.setFontSize(8); + this.doc.setFont('helvetica', 'normal'); + this.doc.text('本報告由 AI 智能評審系統生成', this.margin, this.pageHeight - 10); + this.doc.text(`生成時間:${new Date().toLocaleString('zh-TW')}`, this.margin + 100, this.pageHeight - 10); + + // 生成 PDF Blob + const pdfBlob = this.doc.output('blob'); + resolve(pdfBlob); + } catch (error) { + reject(error); + } + }); + } +} + +// 便捷函數 +export async function generatePDFReport(data: PDFReportData): Promise { + const generator = new PDFReportGenerator(); + return generator.generateReport(data); +} diff --git a/package.json b/package.json index 8a48314..a46e921 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "lint": "next lint", "start": "next start -p 12024", "db:init": "node scripts/init-database-simple.js", - "db:test": "node scripts/test-database.js" + "db:test": "node scripts/test-database.js", + "config:check": "node scripts/check-config.js" }, "dependencies": { "@google/generative-ai": "^0.24.1", @@ -40,6 +41,7 @@ "@radix-ui/react-toggle": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-tooltip": "1.1.6", + "@types/qrcode": "^1.5.5", "@vercel/analytics": "latest", "adm-zip": "^0.5.16", "autoprefixer": "^10.4.20", @@ -49,7 +51,10 @@ "date-fns": "4.1.0", "embla-carousel-react": "8.5.1", "geist": "latest", + "html2canvas": "^1.4.1", "input-otp": "1.4.1", + "jspdf": "^3.0.3", + "jspdf-autotable": "^5.0.2", "lucide-react": "^0.454.0", "mammoth": "^1.11.0", "mysql2": "^3.15.0", @@ -57,6 +62,8 @@ "next-themes": "^0.4.6", "node-fetch": "^3.3.2", "node-pptx": "^1.0.0", + "puppeteer": "^24.22.1", + "qrcode": "^1.5.4", "react": "^18", "react-day-picker": "9.8.0", "react-dom": "^18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4dbb15..9b48347 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: '@radix-ui/react-tooltip': specifier: 1.1.6 version: 1.1.6(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.5 '@vercel/analytics': specifier: latest version: 1.5.0(next@14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0))(react@18.0.0) @@ -122,9 +125,18 @@ importers: geist: specifier: latest version: 1.5.1(next@14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0)) + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 input-otp: specifier: 1.4.1 version: 1.4.1(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + jspdf: + specifier: ^3.0.3 + version: 3.0.3 + jspdf-autotable: + specifier: ^5.0.2 + version: 5.0.2(jspdf@3.0.3) lucide-react: specifier: ^0.454.0 version: 0.454.0(react@18.0.0) @@ -146,6 +158,12 @@ importers: node-pptx: specifier: ^1.0.0 version: 1.0.0 + puppeteer: + specifier: ^24.22.1 + version: 24.22.1(typescript@5.0.2) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 react: specifier: ^18 version: 18.0.0 @@ -218,6 +236,18 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + 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/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@date-fns/tz@1.2.0': resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} @@ -319,6 +349,11 @@ packages: cpu: [x64] os: [win32] + '@puppeteer/browsers@2.10.10': + resolution: {integrity: sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==} + engines: {node: '>=18'} + hasBin: true + '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -1302,6 +1337,9 @@ packages: '@tailwindcss/postcss@4.1.9': resolution: {integrity: sha512-v3DKzHibZO8ioVDmuVHCW1PR0XSM7nS40EjZFJEA1xPuvTuQPaR5flE1LyikU3hu2u1KNWBtEaSe8qsQjX3tyg==} + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1332,9 +1370,18 @@ packages: '@types/node@22.0.0': resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/qrcode@1.5.5': + resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/react-dom@18.0.0': resolution: {integrity: sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg==} @@ -1344,9 +1391,15 @@ packages: '@types/scheduler@0.26.0': resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@vercel/analytics@1.5.0': resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==} peerDependencies: @@ -1381,16 +1434,35 @@ packages: resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} engines: {node: '>=12.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + 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.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + attr-accept@2.2.5: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} @@ -1406,6 +1478,51 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + b4a@1.7.2: + resolution: {integrity: sha512-DyUOdz+E8R6+sruDpQNOaV0y/dBbV6X/8ZkxrDcR0Ifc3BgKlpgG0VAtfOozA0eMtJO5GGe9FsZhueLs00pTww==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + bare-events@2.7.0: + resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==} + + bare-fs@4.4.4: + resolution: {integrity: sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.2.2: + resolution: {integrity: sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==} + + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1413,6 +1530,10 @@ packages: resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==} hasBin: true + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -1424,23 +1545,50 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001743: resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + chromium-bidi@8.0.0: + resolution: {integrity: sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==} + peerDependencies: + devtools-protocol: '*' + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1451,9 +1599,31 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + 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==} + + core-js@3.45.1: + resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1509,18 +1679,39 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + date-fns-jalali@4.1.0-0: resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} deferred@0.7.11: resolution: {integrity: sha512-8eluCl/Blx4YOGwMapBvXRKxHXhA8ejDXYzEaK8+/gtcm8hRMhSLmXSqDmNUKNc/C8HNSmuyyp/hflhqDAvK2A==} + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -1532,9 +1723,18 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devtools-protocol@0.0.1495869: + resolution: {integrity: sha512-i+bkd9UYFis40RcnkW7XrOprCujXRAHg62IVh/Ah3G8MmNXpCGt1m0dTFhSdx/AVs8XEMbdOGRwdkR1Bcta8AA==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + duck@0.1.12: resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} @@ -1554,6 +1754,12 @@ packages: 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==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -1561,6 +1767,13 @@ packages: entities@1.1.2: resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-toolkit@1.39.10: resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} @@ -1579,23 +1792,61 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + esniff@2.0.1: resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} engines: {node: '>=0.10'} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-selector@2.1.2: resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} engines: {node: '>= 12'} @@ -1603,6 +1854,10 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1628,10 +1883,22 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1649,6 +1916,18 @@ packages: resolution: {integrity: sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==} deprecated: This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues. + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} @@ -1659,6 +1938,10 @@ packages: immer@10.1.3: resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1672,6 +1955,20 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} @@ -1694,9 +1991,24 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + jsonfile@2.4.0: resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + jspdf-autotable@5.0.2: + resolution: {integrity: sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==} + peerDependencies: + jspdf: ^2 || ^3 + + jspdf@3.0.3: + resolution: {integrity: sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -1770,6 +2082,13 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -1809,11 +2128,17 @@ packages: resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} engines: {node: '>= 18'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mysql2@3.15.0: resolution: {integrity: sha512-tT6pomf5Z/I7Jzxu8sScgrYBMK9bUFWd7Kbo6Fs1L0M13OOIJ/ZobGKS3Z7tQ8Re4lj+LnLXIQVZZxa3fhYKzA==} engines: {node: '>= 8.0'} @@ -1833,6 +2158,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -1886,19 +2215,67 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + option@0.2.4: resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + 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'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -1916,13 +2293,44 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + puppeteer-core@24.22.1: + resolution: {integrity: sha512-2IuC+w6270i8uCr2wGzD9pOi+JUe1CZn4TgPaonsgNryY1BGMcccdO0XBmekFlLYxkqaHrUMzzZnpc+T33mF9g==} + engines: {node: '>=18'} + + puppeteer@24.22.1: + resolution: {integrity: sha512-jEeKDdJxXrv8Ki0I3q0amyv2FXZhucRG0Oh0EHF+igx8FiPG54y/E97j0+4WxITI7EjwoSxZFyxh61ZJ+TUo2w==} + engines: {node: '>=18'} + hasBin: true + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + react-day-picker@9.8.0: resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==} engines: {node: '>=18'} @@ -2020,9 +2428,27 @@ packages: redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -2035,12 +2461,32 @@ packages: scheduler@0.21.0: resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner@1.7.4: resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} peerDependencies: @@ -2051,6 +2497,10 @@ packages: 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'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -2058,13 +2508,28 @@ packages: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -2078,6 +2543,10 @@ packages: babel-plugin-macros: optional: true + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + tailwind-merge@2.5.5: resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} @@ -2093,10 +2562,22 @@ packages: resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} engines: {node: '>=6'} + tar-fs@3.1.1: + resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2124,6 +2605,9 @@ packages: type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typed-query-selector@2.12.0: + resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} + typescript@5.0.2: resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} engines: {node: '>=12.20'} @@ -2169,6 +2653,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + vaul@0.9.9: resolution: {integrity: sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==} peerDependencies: @@ -2182,6 +2669,35 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webdriver-bidi-protocol@0.2.11: + resolution: {integrity: sha512-Y9E1/oi4XMxcR8AT0ZC4OvYntl34SPgwjmELH+owjBr0korAX4jKgZULBWILGCVGdVCQ0dodTToIETozhG8zvA==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + 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'} + + 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 + xml2js@0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} engines: {node: '>=4.0.0'} @@ -2198,10 +2714,36 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + zod@3.25.67: resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} @@ -2214,6 +2756,16 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/runtime@7.28.4': {} + '@date-fns/tz@1.2.0': {} '@floating-ui/core@1.7.3': @@ -2286,6 +2838,20 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.16': optional: true + '@puppeteer/browsers@2.10.10': + dependencies: + debug: 4.4.3 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.2 + tar-fs: 3.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-buffer + - react-native-b4a + - supports-color + '@radix-ui/number@1.1.0': {} '@radix-ui/number@1.1.1': {} @@ -3254,6 +3820,8 @@ snapshots: postcss: 8.5.0 tailwindcss: 4.1.9 + '@tootallnate/quickjs-emscripten@0.23.0': {} + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -3282,8 +3850,17 @@ snapshots: dependencies: undici-types: 6.11.1 + '@types/pako@2.0.4': {} + '@types/prop-types@15.7.15': {} + '@types/qrcode@1.5.5': + dependencies: + '@types/node': 22.0.0 + + '@types/raf@3.4.3': + optional: true + '@types/react-dom@18.0.0': dependencies: '@types/react': 18.0.0 @@ -3296,8 +3873,16 @@ snapshots: '@types/scheduler@0.26.0': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/use-sync-external-store@0.0.6': {} + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.0.0 + optional: true + '@vercel/analytics@1.5.0(next@14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0))(react@18.0.0)': optionalDependencies: next: 14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0) @@ -3307,16 +3892,30 @@ snapshots: adm-zip@0.5.16: {} + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + any-promise@1.3.0: {} argparse@1.0.10: dependencies: sprintf-js: 1.0.3 + argparse@2.0.1: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + attr-accept@2.2.5: {} autoprefixer@10.4.20(postcss@8.5.0): @@ -3331,10 +3930,51 @@ snapshots: aws-ssl-profiles@1.1.2: {} + b4a@1.7.2: {} + + bare-events@2.7.0: {} + + bare-fs@4.4.4: + dependencies: + bare-events: 2.7.0 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.7.0) + bare-url: 2.2.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - react-native-b4a + optional: true + + bare-os@3.6.2: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + optional: true + + bare-stream@2.7.0(bare-events@2.7.0): + dependencies: + streamx: 2.23.0 + optionalDependencies: + bare-events: 2.7.0 + transitivePeerDependencies: + - react-native-b4a + optional: true + + bare-url@2.2.2: + dependencies: + bare-path: 3.0.0 + optional: true + + base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.8.6: {} + basic-ftp@5.0.5: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -3349,20 +3989,56 @@ snapshots: node-releases: 2.0.21 update-browserslist-db: 1.1.3(browserslist@4.26.2) + buffer-crc32@0.2.13: {} + busboy@1.6.0: dependencies: streamsearch: 1.1.0 + callsites@3.1.0: {} + + camelcase@5.3.1: {} + caniuse-lite@1.0.30001743: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.4 + '@types/raf': 3.4.3 + core-js: 3.45.1 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + chownr@3.0.0: {} + chromium-bidi@8.0.0(devtools-protocol@0.0.1495869): + dependencies: + devtools-protocol: 0.0.1495869 + mitt: 3.0.1 + zod: 3.25.67 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} cmdk@1.0.4(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0): @@ -3377,8 +4053,30 @@ snapshots: - '@types/react' - '@types/react-dom' + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + core-js@3.45.1: + optional: true + core-util-is@1.0.3: {} + cosmiconfig@9.0.0(typescript@5.0.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.0.2 + + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + csstype@3.1.3: {} d3-array@3.2.4: @@ -3426,10 +4124,18 @@ snapshots: data-uri-to-buffer@4.0.1: {} + data-uri-to-buffer@6.0.2: {} + date-fns-jalali@4.1.0-0: {} date-fns@4.1.0: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + decimal.js-light@2.5.1: {} deferred@0.7.11: @@ -3440,14 +4146,29 @@ snapshots: next-tick: 1.1.0 timers-ext: 0.1.8 + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + denque@2.1.0: {} detect-libc@2.1.0: {} detect-node-es@1.1.0: {} + devtools-protocol@0.0.1495869: {} + + dijkstrajs@1.0.3: {} + dingbat-to-unicode@1.0.1: {} + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + duck@0.1.12: dependencies: underscore: 1.13.7 @@ -3466,6 +4187,12 @@ snapshots: embla-carousel@8.5.1: {} + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -3473,6 +4200,12 @@ snapshots: entities@1.1.2: {} + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + es-toolkit@1.39.10: {} es5-ext@0.10.64: @@ -3495,6 +4228,14 @@ snapshots: escalade@3.2.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + esniff@2.0.1: dependencies: d: 1.0.2 @@ -3502,6 +4243,12 @@ snapshots: event-emitter: 0.3.5 type: 2.7.3 + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + event-emitter@0.3.5: dependencies: d: 1.0.2 @@ -3509,21 +4256,54 @@ snapshots: eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.7.0 + ext@1.7.0: dependencies: type: 2.7.3 + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-fifo@1.3.2: {} + + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} + file-selector@2.1.2: dependencies: tslib: 2.8.1 file-uri-to-path@1.0.0: {} + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -3553,8 +4333,22 @@ snapshots: dependencies: is-property: 1.0.2 + get-caller-file@2.0.5: {} + get-nonce@1.0.1: {} + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + graceful-fs@4.2.11: {} hoek@4.3.1: {} @@ -3563,6 +4357,25 @@ snapshots: hoek@6.1.3: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 @@ -3571,6 +4384,11 @@ snapshots: immer@10.1.3: {} + 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.0.0(react@18.0.0))(react@18.0.0): @@ -3580,6 +4398,14 @@ snapshots: internmap@2.0.3: {} + iobuffer@5.4.0: {} + + ip-address@10.0.1: {} + + is-arrayish@0.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + is-property@1.0.2: {} isarray@1.0.0: {} @@ -3598,10 +4424,31 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + jsonfile@2.4.0: optionalDependencies: graceful-fs: 4.2.11 + jspdf-autotable@5.0.2(jspdf@3.0.3): + dependencies: + jspdf: 3.0.3 + + jspdf@3.0.3: + dependencies: + '@babel/runtime': 7.28.4 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.45.1 + dompurify: 3.2.7 + html2canvas: 1.4.1 + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -3662,6 +4509,12 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + long@5.3.2: {} loose-envify@1.4.0: @@ -3705,8 +4558,12 @@ snapshots: dependencies: minipass: 7.1.2 + mitt@3.0.1: {} + mkdirp@3.0.1: {} + ms@2.1.3: {} + mysql2@3.15.0: dependencies: aws-ssl-profiles: 1.1.2 @@ -3733,6 +4590,8 @@ snapshots: nanoid@3.3.11: {} + netmask@2.0.2: {} + next-themes@0.4.6(react-dom@18.0.0(react@18.0.0))(react@18.0.0): dependencies: react: 18.0.0 @@ -3791,14 +4650,68 @@ snapshots: object-assign@4.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + option@0.2.4: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + pako@1.0.11: {} + pako@2.1.0: {} + + 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.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + pend@1.2.0: {} + + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} + pngjs@5.0.0: {} + postcss-value-parser@4.2.0: {} postcss@8.4.31: @@ -3822,14 +4735,79 @@ snapshots: process-nextick-args@2.0.1: {} + progress@2.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} + puppeteer-core@24.22.1: + dependencies: + '@puppeteer/browsers': 2.10.10 + chromium-bidi: 8.0.0(devtools-protocol@0.0.1495869) + debug: 4.4.3 + devtools-protocol: 0.0.1495869 + typed-query-selector: 2.12.0 + webdriver-bidi-protocol: 0.2.11 + ws: 8.18.3 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + + puppeteer@24.22.1(typescript@5.0.2): + dependencies: + '@puppeteer/browsers': 2.10.10 + chromium-bidi: 8.0.0(devtools-protocol@0.0.1495869) + cosmiconfig: 9.0.0(typescript@5.0.2) + devtools-protocol: 0.0.1495869 + puppeteer-core: 24.22.1 + typed-query-selector: 2.12.0 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - typescript + - utf-8-validate + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + react-day-picker@9.8.0(react@18.0.0): dependencies: '@date-fns/tz': 1.2.0 @@ -3937,8 +4915,20 @@ snapshots: redux@5.0.1: {} + regenerator-runtime@0.13.11: + optional: true + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + reselect@5.1.1: {} + resolve-from@4.0.0: {} + + rgbcolor@1.0.1: + optional: true + safe-buffer@5.1.2: {} safer-buffer@2.1.2: {} @@ -3949,10 +4939,29 @@ snapshots: dependencies: loose-envify: 1.4.0 + semver@7.7.2: {} + seq-queue@0.0.5: {} + set-blocking@2.0.0: {} + setimmediate@1.0.5: {} + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.0.1 + smart-buffer: 4.2.0 + sonner@1.7.4(react-dom@18.0.0(react@18.0.0))(react@18.0.0): dependencies: react: 18.0.0 @@ -3960,21 +4969,48 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: + optional: true + sprintf-js@1.0.3: {} sqlstring@2.3.3: {} + stackblur-canvas@2.7.0: + optional: true + streamsearch@1.1.0: {} + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + styled-jsx@5.1.1(react@18.0.0): dependencies: client-only: 0.0.1 react: 18.0.0 + svg-pathdata@6.0.3: + optional: true + tailwind-merge@2.5.5: {} tailwindcss-animate@1.0.7(tailwindcss@4.1.9): @@ -3985,6 +5021,25 @@ snapshots: tapable@2.2.3: {} + tar-fs@3.1.1: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.4.4 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + - react-native-b4a + + tar-stream@3.1.7: + dependencies: + b4a: 1.7.2 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - react-native-b4a + tar@7.4.3: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -3994,6 +5049,16 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + text-decoder@1.2.3: + dependencies: + b4a: 1.7.2 + transitivePeerDependencies: + - react-native-b4a + + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -4019,6 +5084,8 @@ snapshots: type@2.7.3: {} + typed-query-selector@2.12.0: {} + typescript@5.0.2: {} underscore@1.13.7: {} @@ -4052,6 +5119,10 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + vaul@0.9.9(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0): dependencies: '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) @@ -4080,6 +5151,26 @@ snapshots: web-streams-polyfill@3.3.3: {} + webdriver-bidi-protocol@0.2.11: {} + + which-module@2.0.1: {} + + 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 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + xml2js@0.4.23: dependencies: sax: 1.4.1 @@ -4095,6 +5186,46 @@ snapshots: xmlbuilder@11.0.1: {} + y18n@4.0.3: {} + + y18n@5.0.8: {} + yallist@5.0.0: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + 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 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + zod@3.25.67: {} diff --git a/scripts/check-config.js b/scripts/check-config.js new file mode 100644 index 0000000..d9c5ada --- /dev/null +++ b/scripts/check-config.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +/** + * 配置檢查工具 + * 用於驗證環境變量配置是否正確 + */ + +console.log('🔍 檢查環境變量配置...\n'); + +// 檢查環境變量 +const envVars = { + 'NEXT_PUBLIC_APP_URL': process.env.NEXT_PUBLIC_APP_URL, + 'NEXT_PUBLIC_APP_NAME': process.env.NEXT_PUBLIC_APP_NAME, + 'DB_HOST': process.env.DB_HOST, + 'DB_PORT': process.env.DB_PORT, + 'DB_NAME': process.env.DB_NAME, + 'DB_USER': process.env.DB_USER, + 'DB_PASSWORD': process.env.DB_PASSWORD ? '***已設置***' : undefined, + 'GEMINI_API_KEY': process.env.GEMINI_API_KEY ? '***已設置***' : undefined, + 'GEMINI_MODEL': process.env.GEMINI_MODEL, + 'GEMINI_MAX_TOKENS': process.env.GEMINI_MAX_TOKENS, +}; + +console.log('📋 當前環境變量:'); +let hasErrors = false; + +Object.entries(envVars).forEach(([key, value]) => { + const status = value ? '✅' : '❌'; + const displayValue = value || '未設置'; + console.log(` ${status} ${key}: ${displayValue}`); + if (!value && key !== 'GEMINI_MAX_TOKENS') { + hasErrors = true; + } +}); + +console.log(''); + +if (hasErrors) { + console.log('❌ 發現配置問題,請設置缺少的環境變量'); + console.log('\n💡 建議的 .env.local 配置:'); +} else { + console.log('✅ 所有必要的環境變量都已設置'); + console.log('\n🔧 建議的 .env.local 配置:'); +} + +console.log('NEXT_PUBLIC_APP_URL=http://localhost:12024'); +console.log('NEXT_PUBLIC_APP_NAME=AI 智能評審系統'); +console.log('DB_HOST=mysql.theaken.com'); +console.log('DB_PORT=33306'); +console.log('DB_NAME=db_AI_scoring'); +console.log('DB_USER=root'); +console.log('DB_PASSWORD=zh6161168'); +console.log('GEMINI_API_KEY=AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4'); +console.log('GEMINI_MODEL=gemini-1.5-pro'); +console.log('GEMINI_MAX_TOKENS=8192'); + +console.log('\n📝 使用說明:'); +console.log('1. 複製 env.example 到 .env.local'); +console.log('2. 根據需要修改配置'); +console.log('3. 重啟開發服務器: npm run dev'); \ No newline at end of file diff --git a/uploads/projects/20/1758628097639_yyc3losvt.pptx b/uploads/projects/20/1758628097639_yyc3losvt.pptx deleted file mode 100644 index 8b38ba6..0000000 Binary files a/uploads/projects/20/1758628097639_yyc3losvt.pptx and /dev/null differ diff --git a/uploads/projects/21/1758631225489_8g5whd8li.pptx b/uploads/projects/21/1758631225489_8g5whd8li.pptx deleted file mode 100644 index 8b38ba6..0000000 Binary files a/uploads/projects/21/1758631225489_8g5whd8li.pptx and /dev/null differ diff --git a/uploads/projects/22/1758631398314_vfgwct81t.pptx b/uploads/projects/22/1758631398314_vfgwct81t.pptx deleted file mode 100644 index 8b38ba6..0000000 Binary files a/uploads/projects/22/1758631398314_vfgwct81t.pptx and /dev/null differ diff --git a/uploads/projects/24/1758637006045_3d4e2i2yb.pptx b/uploads/projects/24/1758637006045_3d4e2i2yb.pptx deleted file mode 100644 index 8b38ba6..0000000 Binary files a/uploads/projects/24/1758637006045_3d4e2i2yb.pptx and /dev/null differ diff --git a/uploads/projects/25/1758640239292_xue04wm9w.pptx b/uploads/projects/25/1758640239292_xue04wm9w.pptx deleted file mode 100644 index 8b38ba6..0000000 Binary files a/uploads/projects/25/1758640239292_xue04wm9w.pptx and /dev/null differ diff --git a/uploads/projects/26/1758640840561_g35s5avq9.pptx b/uploads/projects/26/1758640840561_g35s5avq9.pptx deleted file mode 100644 index 8b38ba6..0000000 Binary files a/uploads/projects/26/1758640840561_g35s5avq9.pptx and /dev/null differ diff --git a/uploads/projects/27/1758640969869_zwgnqodtb.pptx b/uploads/projects/27/1758640969869_zwgnqodtb.pptx deleted file mode 100644 index 8b38ba6..0000000 Binary files a/uploads/projects/27/1758640969869_zwgnqodtb.pptx and /dev/null differ diff --git a/uploads/projects/28/1758641041760_2bbp7o2tl.pptx b/uploads/projects/28/1758641041760_2bbp7o2tl.pptx deleted file mode 100644 index abb95cd..0000000 Binary files a/uploads/projects/28/1758641041760_2bbp7o2tl.pptx and /dev/null differ diff --git a/uploads/projects/19/1758627771858_1bu28tdfr.pptx b/uploads/projects/29/1758642266698_v8881ji0n.pptx similarity index 100% rename from uploads/projects/19/1758627771858_1bu28tdfr.pptx rename to uploads/projects/29/1758642266698_v8881ji0n.pptx