diff --git a/app/api/admin/awards/route.ts b/app/api/admin/awards/route.ts index b9116a9..ffb55cf 100644 --- a/app/api/admin/awards/route.ts +++ b/app/api/admin/awards/route.ts @@ -149,9 +149,12 @@ export async function GET(request: NextRequest) { competitionId: award.competition_id, competitionName: (award as any).competition_name, competitionType: (award as any).competition_type, + competitionDescription: (award as any).competition_description, + competitionStartDate: (award as any).competition_start_date, + competitionEndDate: (award as any).competition_end_date, awardName: award.award_name, awardType: award.award_type, - teamName: award.team_name, + teamName: (award as any).team_name_from_teams || award.team_name, appName: award.app_name, applicationLinks: award.application_links ? JSON.parse(award.application_links) : null, documents: award.documents ? JSON.parse(award.documents) : [], diff --git a/app/api/awards/[id]/scores/route.ts b/app/api/awards/[id]/scores/route.ts new file mode 100644 index 0000000..57603b1 --- /dev/null +++ b/app/api/awards/[id]/scores/route.ts @@ -0,0 +1,211 @@ +// ===================================================== +// 獲取獎項評分詳情 API +// ===================================================== + +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/database'; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const awardId = params.id; + + console.log('🔍 獲取獎項評分詳情:', awardId); + + // 先獲取獎項資訊 + const awardSql = 'SELECT * FROM awards WHERE id = ?'; + console.log('🔍 執行 SQL:', awardSql, '參數:', [awardId]); + + const award = await db.queryOne(awardSql, [awardId]); + console.log('📊 查詢結果:', award); + + if (!award) { + console.log('❌ 找不到獎項:', awardId); + return NextResponse.json({ + success: false, + message: '找不到指定的獎項', + data: [] + }); + } + + console.log('📊 獎項資訊:', { + id: award.id, + competitionType: award.competition_type, + appId: award.app_id, + proposalId: award.proposal_id + }); + + let scores = []; + + // 根據競賽類型查詢對應的評分表 + if (award.competition_type === 'individual' && award.app_id) { + // 查詢 app_judge_scores + const sql = ` + SELECT + ajs.*, + j.name as judge_name, + j.title as judge_title, + j.avatar as judge_avatar, + j.department as judge_department + FROM app_judge_scores ajs + LEFT JOIN judges j ON ajs.judge_id = j.id + WHERE ajs.app_id = ? + ORDER BY ajs.created_at DESC + `; + console.log('🔍 執行個人賽 SQL:', sql, '參數:', [award.app_id]); + scores = await db.query(sql, [award.app_id]); + console.log('📊 從 app_judge_scores 獲取到:', scores.length, '筆'); + } else if (award.competition_type === 'proposal' && award.proposal_id) { + // 查詢 proposal_judge_scores + const sql = ` + SELECT + pjs.*, + j.name as judge_name, + j.title as judge_title, + j.avatar as judge_avatar, + j.department as judge_department + FROM proposal_judge_scores pjs + LEFT JOIN judges j ON pjs.judge_id = j.id + WHERE pjs.proposal_id = ? + ORDER BY pjs.created_at DESC + `; + console.log('🔍 執行提案賽 SQL:', sql, '參數:', [award.proposal_id]); + scores = await db.query(sql, [award.proposal_id]); + console.log('📊 從 proposal_judge_scores 獲取到:', scores.length, '筆'); + } else if (award.competition_type === 'team' && award.app_id) { + // 查詢 judge_scores (團隊賽) + const sql = ` + SELECT + js.*, + j.name as judge_name, + j.title as judge_title, + j.avatar as judge_avatar, + j.department as judge_department + FROM judge_scores js + LEFT JOIN judges j ON js.judge_id = j.id + WHERE js.app_id = ? + ORDER BY js.submitted_at DESC + `; + console.log('🔍 執行團隊賽 SQL:', sql, '參數:', [award.app_id]); + scores = await db.query(sql, [award.app_id]); + console.log('📊 從 judge_scores 獲取到:', scores.length, '筆'); + } else { + console.log('❌ 無法確定評分表類型:', { + competitionType: award.competition_type, + hasAppId: !!award.app_id, + hasProposalId: !!award.proposal_id, + hasTeamId: !!award.team_id + }); + } + + // 處理評分資料 + const processedScores = scores.map((score: any) => { + // 根據不同的評分表處理不同的欄位 + let overallScore = 0; + let criteria: Array<{name: string, score: number, maxScore: number}> = []; + + if (award.competition_type === 'individual') { + // app_judge_scores 的處理 + overallScore = score.total_score || 0; + criteria = [ + { name: '創新性', score: score.innovation_score, maxScore: 10 }, + { name: '技術性', score: score.technical_score, maxScore: 10 }, + { name: '可用性', score: score.usability_score, maxScore: 10 }, + { name: '展示性', score: score.presentation_score, maxScore: 10 }, + { name: '影響力', score: score.impact_score, maxScore: 10 } + ]; + } else if (award.competition_type === 'proposal') { + // proposal_judge_scores 的處理 + overallScore = score.total_score || 0; + criteria = [ + { name: '問題識別', score: score.problem_identification_score, maxScore: 10 }, + { name: '解決方案可行性', score: score.solution_feasibility_score, maxScore: 10 }, + { name: '創新性', score: score.innovation_score, maxScore: 10 }, + { name: '影響力', score: score.impact_score, maxScore: 10 }, + { name: '展示性', score: score.presentation_score, maxScore: 10 } + ]; + } else if (award.competition_type === 'team') { + // judge_scores (團隊賽) 的處理 + overallScore = score.overall_score || score.total_score || 0; + // 嘗試解析 criteria 欄位,如果沒有則使用預設的團隊評分標準 + if (score.criteria) { + try { + criteria = typeof score.criteria === 'string' + ? JSON.parse(score.criteria) + : score.criteria; + } catch (e) { + console.error('解析 criteria 失敗:', e); + criteria = []; + } + } + + // 如果沒有 criteria 或解析失敗,使用預設的團隊評分標準 + if (!criteria || criteria.length === 0) { + criteria = [ + { name: '團隊合作', score: score.teamwork_score || 0, maxScore: 10 }, + { name: '創新性', score: score.innovation_score || 0, maxScore: 10 }, + { name: '技術實作', score: score.technical_score || 0, maxScore: 10 }, + { name: '展示能力', score: score.presentation_score || 0, maxScore: 10 }, + { name: '整體表現', score: score.overall_performance || 0, maxScore: 10 } + ]; + } + } + + return { + id: score.id, + awardId: awardId, + judgeId: score.judge_id, + judgeName: score.judge_name || '未知評審', + judgeTitle: score.judge_title || '評審', + judgeAvatar: score.judge_avatar, + judgeDepartment: score.judge_department, + overallScore: overallScore, + submittedAt: score.submitted_at || score.created_at, + comment: score.comment || score.comments || '', + criteria: criteria, + }; + }); + + console.log('✅ 處理後的評分資料:', processedScores.length, '筆'); + + return NextResponse.json({ + success: true, + message: '評分詳情獲取成功', + data: processedScores + }); + + } catch (error) { + console.error('❌ 獲取評分詳情失敗:', error); + console.error('❌ 錯誤詳情:', { + message: error instanceof Error ? error.message : '未知錯誤', + stack: error instanceof Error ? error.stack : undefined, + awardId: params.id + }); + + // 檢查評分表是否存在 + try { + const tableCheck = await db.query("SHOW TABLES LIKE 'judge_scores'"); + console.log('🔍 judge_scores 表存在檢查:', tableCheck); + + if (tableCheck.length > 0) { + // 檢查表結構 + const structure = await db.query("DESCRIBE judge_scores"); + console.log('🔍 judge_scores 表結構:', structure); + } + } catch (tableError) { + console.error('❌ 檢查表存在性失敗:', tableError); + } + + return NextResponse.json({ + success: false, + message: '獲取評分詳情失敗', + error: error instanceof Error ? error.message : '未知錯誤', + details: { + awardId: params.id, + errorType: error instanceof Error ? error.constructor.name : 'Unknown' + } + }, { status: 500 }); + } +} diff --git a/app/competition/page.tsx b/app/competition/page.tsx index b0421f6..4833658 100644 --- a/app/competition/page.tsx +++ b/app/competition/page.tsx @@ -451,7 +451,10 @@ export default function CompetitionPage() { - {award.appName || award.proposalTitle || award.teamName} + {award.competitionType === "team" + ? (award.teamName || "團隊名稱") + : (award.appName || award.proposalTitle || "應用名稱") + }

by {award.creator}

diff --git a/app/page.tsx b/app/page.tsx index 613f69b..47307b7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -148,7 +148,7 @@ export default function AIShowcasePlatform() { canAccessAdmin, } = useAuth() - const { competitions, awards, getAwardsByYear, getCompetitionRankings } = useCompetition() + const { competitions, awards, getAwardsByYear, getCompetitionRankings, loadingAwards, getAvailableYears } = useCompetition() const [showLogin, setShowLogin] = useState(false) const [showRegister, setShowRegister] = useState(false) @@ -193,6 +193,17 @@ export default function AIShowcasePlatform() { showAwardDetail }); + // 動態設定初始年份 + useEffect(() => { + if (awards.length > 0 && selectedYear === 2024) { + const availableYears = getAvailableYears(); + if (availableYears.length > 0) { + setSelectedYear(availableYears[0]); + console.log('🎯 自動設定年份為:', availableYears[0]); + } + } + }, [awards, selectedYear, getAvailableYears]); + // 載入應用數據 const loadApps = async () => { try { @@ -595,8 +606,11 @@ export default function AIShowcasePlatform() { - 2024年 - 2023年 + {getAvailableYears().map((year) => ( + + {year}年 + + ))}
@@ -720,24 +734,26 @@ export default function AIShowcasePlatform() { {/* Statistics */}
-
{filteredAwards.length}
+
+ {loadingAwards ? '...' : filteredAwards.length} +
總獎項數
- {filteredAwards.filter((a) => a.rank > 0 && a.rank <= 3).length} + {loadingAwards ? '...' : filteredAwards.filter((a) => a.rank > 0 && a.rank <= 3).length}
前三名獎項
- {filteredAwards.filter((a) => a.awardType === "popular").length} + {loadingAwards ? '...' : filteredAwards.filter((a) => a.awardType === "popular").length}
人氣獎項
- {new Set(filteredAwards.map((a) => `${a.year}-${a.month}`)).size} + {loadingAwards ? '...' : new Set(filteredAwards.map((a) => `${a.year}-${a.month}`)).size}
競賽場次
@@ -747,7 +763,12 @@ export default function AIShowcasePlatform() { {/* Awards Grid with Enhanced Display */} - {filteredAwards.length > 0 ? ( + {loadingAwards ? ( +
+
+

載入獎項資料中...

+
+ ) : filteredAwards.length > 0 ? (
{/* Group awards by month */} {Array.from(new Set(filteredAwards.map((award) => award.month))) diff --git a/components/admin/competition-management.tsx b/components/admin/competition-management.tsx index 9f39609..6f61099 100644 --- a/components/admin/competition-management.tsx +++ b/components/admin/competition-management.tsx @@ -102,7 +102,7 @@ export function CompetitionManagement() { // 可用用戶狀態 const [availableUsers, setAvailableUsers] = useState([]) const [isLoadingUsers, setIsLoadingUsers] = useState(false) - + // 獎項資料庫整合狀態 const [dbAwards, setDbAwards] = useState([]) const [isLoadingAwards, setIsLoadingAwards] = useState(false) @@ -2132,7 +2132,7 @@ export function CompetitionManagement() { setIsLoadingAwards(false) } } - + // 載入競賽相關數據用於評分 const loadCompetitionDataForScoring = async (competition: any) => { try { @@ -7877,14 +7877,14 @@ export function CompetitionManagement() { if (value === "custom-input") { setNewAward({ ...newAward, customAwardTypeId: "", awardName: "" }) } else { - // 檢查是否為自定義獎項類型 - const customAwardType = competitionAwardTypes.find(type => type.id === value) - if (customAwardType) { - setNewAward({ - ...newAward, - customAwardTypeId: value, - awardName: customAwardType.name - }) + // 檢查是否為自定義獎項類型 + const customAwardType = competitionAwardTypes.find(type => type.id === value) + if (customAwardType) { + setNewAward({ + ...newAward, + customAwardTypeId: value, + awardName: customAwardType.name + }) } } }} @@ -7896,12 +7896,12 @@ export function CompetitionManagement() { 自定義輸入 {competitionAwardTypes.map((awardType) => ( - -
- {awardType.icon} - {awardType.name} -
-
+ +
+ {awardType.icon} + {awardType.name} +
+
))}
diff --git a/components/competition/award-detail-dialog-backup.tsx b/components/competition/award-detail-dialog-backup.tsx new file mode 100644 index 0000000..e1f15ac --- /dev/null +++ b/components/competition/award-detail-dialog-backup.tsx @@ -0,0 +1,908 @@ +"use client" + +import { useState, useEffect } from "react" +import { useCompetition } from "@/contexts/competition-context" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Progress } from "@/components/ui/progress" +import { + Target, + Users, + Lightbulb, + Trophy, + Crown, + Award, + Camera, + ImageIcon, + ChevronLeft, + ChevronRight, + X, + Star, + MessageSquare, + BarChart3, + ExternalLink, + Eye, + Link, + FileText, + Download, +} from "lucide-react" +import type { Award as AwardType } from "@/types/competition" + +interface AwardDetailDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + award: AwardType +} + +// Judge scoring data - will be loaded from API + +// App links and reports data - empty for production +const getAppData = (awardId: string) => { + return { + appUrl: "", + demoUrl: "", + githubUrl: "", + reports: [], + } +} + + +export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDialogProps) { + const { competitions, judges, getTeamById, getProposalById } = useCompetition() + const [activeTab, setActiveTab] = useState("overview") + const [showPhotoGallery, setShowPhotoGallery] = useState(false) + const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0) + const [competitionJudges, setCompetitionJudges] = useState([]) + const [judgeScores, setJudgeScores] = useState([]) + const [loadingScores, setLoadingScores] = useState(false) + + // 添加調試資訊 + console.log('🏆 AwardDetailDialog 渲染:', { + open, + award: award ? { + id: award.id, + competitionId: award.competitionId, + awardName: award.awardName, + hasCompetitionId: !!award.competitionId + } : null + }); + + const competition = competitions.find((c) => c.id === award.competitionId) + const appData = getAppData(award.id) + + // 載入競賽評審團資訊 + useEffect(() => { + console.log('🔍 useEffect 觸發:', { open, competitionId: award.competitionId, awardId: award.id }); + + if (open && award.competitionId) { + const loadCompetitionJudges = async (retryCount = 0) => { + try { + console.log('🔍 載入競賽評審團:', award.competitionId, '重試次數:', retryCount); + console.log('🏆 獎項資料:', award); + + // 添加時間戳防止快取,並設置快取控制標頭 + const timestamp = Date.now(); + const apiUrl = `/api/competitions/${award.competitionId}/judges?t=${timestamp}`; + console.log('🌐 API URL:', apiUrl); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }); + + console.log('📡 API 回應狀態:', response.status); + console.log('📡 API 回應標頭:', Object.fromEntries(response.headers.entries())); + + if (!response.ok) { + const errorText = await response.text(); + console.error('❌ API 錯誤回應:', errorText); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); + } + + const data = await response.json(); + console.log('📄 API 回應資料:', JSON.stringify(data, null, 2)); + + if (data.success && data.data && data.data.judges) { + console.log('✅ 獲取到評審團:', data.data.judges.length, '位'); + console.log('👥 評審團詳細資料:', data.data.judges); + setCompetitionJudges(data.data.judges); + } else { + console.error('❌ 獲取評審團失敗:', data.message || '未知錯誤'); + console.error('❌ 完整錯誤資料:', data); + + // 如果沒有評審資料且是第一次嘗試,重試一次 + if (retryCount === 0) { + console.log('🔄 重試載入評審團...'); + setTimeout(() => loadCompetitionJudges(1), 1000); + } else { + setCompetitionJudges([]); + } + } + } catch (error) { + console.error('❌ 載入評審團失敗:', error); + + // 如果是網路錯誤且是第一次嘗試,重試一次 + if (retryCount === 0) { + console.log('🔄 網路錯誤,重試載入評審團...'); + setTimeout(() => loadCompetitionJudges(1), 2000); + } else { + setCompetitionJudges([]); + } + } + }; + + // 清空之前的評審資料,確保重新載入 + setCompetitionJudges([]); + loadCompetitionJudges(); + } else { + console.log('❌ useEffect 條件不滿足:', { + open, + competitionId: award.competitionId, + hasCompetitionId: !!award.competitionId + }); + } + }, [open, award.competitionId]); + + // 載入評分詳情 + useEffect(() => { + if (open && award.id) { + const loadJudgeScores = async () => { + try { + setLoadingScores(true); + console.log('🔍 載入評分詳情:', award.id); + + const response = await fetch(`/api/awards/${award.id}/scores`); + const data = await response.json(); + + if (data.success) { + console.log('✅ 載入評分詳情成功:', data.data.length, '筆'); + setJudgeScores(data.data); + } else { + console.error('❌ 載入評分詳情失敗:', data.message); + setJudgeScores([]); + } + } catch (error) { + console.error('❌ 載入評分詳情錯誤:', error); + setJudgeScores([]); + } finally { + setLoadingScores(false); + } + }; + + loadJudgeScores(); + } else { + setJudgeScores([]); + } + }, [open, award.id]); + + // Competition photos - empty for production + const getCompetitionPhotos = () => { + return [] + } + + const competitionPhotos = getCompetitionPhotos() + + const getCompetitionTypeIcon = (type: string) => { + switch (type) { + case "individual": + return + case "team": + return + case "proposal": + return + default: + return + } + } + + const getCompetitionTypeText = (type: string) => { + switch (type) { + case "individual": + return "個人賽" + case "team": + return "團隊賽" + case "proposal": + return "提案賽" + default: + return "競賽" + } + } + + const getCompetitionTypeColor = (type: string) => { + switch (type) { + case "individual": + return "bg-blue-100 text-blue-800 border-blue-200" + case "team": + return "bg-green-100 text-green-800 border-green-200" + case "proposal": + return "bg-purple-100 text-purple-800 border-purple-200" + default: + return "bg-gray-100 text-gray-800 border-gray-200" + } + } + + const getFileIcon = (type: string) => { + switch (type.toLowerCase()) { + case "pdf": + return + case "pptx": + case "ppt": + return + case "docx": + case "doc": + return + default: + return + } + } + + const nextPhoto = () => { + setCurrentPhotoIndex((prev) => (prev + 1) % competitionPhotos.length) + } + + const prevPhoto = () => { + setCurrentPhotoIndex((prev) => (prev - 1 + competitionPhotos.length) % competitionPhotos.length) + } + + const handlePreview = (report: any) => { + // Open preview in new window + window.open(report.previewUrl, "_blank") + } + + const renderAwardOverview = () => ( +
+ + +
+
{award.icon}
+
+ {award.awardName} + + {award.appName || award.proposalTitle || award.teamName} + +
+ + {getCompetitionTypeIcon(award.competitionType)} + {getCompetitionTypeText(award.competitionType)} + + + {award.awardName} + +
+
+
+
+ +
+
+
+ {award.awardType === "popular" && award.competitionType === "team" + ? `${award.score}` + : award.awardType === "popular" + ? `${award.score}` + : award.score} +
+
+ {award.competitionType === "proposal" + ? "評審評分" + : award.awardType === "popular" + ? award.competitionType === "team" + ? "人氣指數" + : "收藏數" + : "評審評分"} +
+
+
+
+ {award.year}年{award.month}月 +
+
獲獎時間
+
+
+
{award.creator}
+
+ {award.competitionType === "team" + ? "團隊" + : award.competitionType === "proposal" + ? "提案團隊" + : "創作者"} +
+
+
+ +
+

+ + 競賽資訊 +

+
+

+ 競賽名稱: + {award.competitionName || competition?.name || '未知競賽'} +

+

+ 競賽描述: + {award.competitionDescription || competition?.description || '暫無描述'} +

+

+ 競賽期間: + {award.competitionStartDate && award.competitionEndDate + ? `${award.competitionStartDate} ~ ${award.competitionEndDate}` + : competition?.startDate && competition?.endDate + ? `${competition.startDate} ~ ${competition.endDate}` + : '暫無時間資訊' + } +

+

+ 評審團: + {competitionJudges && competitionJudges.length > 0 ? ( + + {competitionJudges.length} 位評審 + + ) : ( + 暫無評審信息 + )} +

+
+
+
+
+ + {/* App Links Section */} + + + + + 應用連結 + + 相關應用和資源連結 + + +
+ {(() => { + // 解析 application_links 資料 + let applicationLinks = null; + if (award.applicationLinks) { + applicationLinks = award.applicationLinks; + } else if (award.application_links) { + try { + applicationLinks = typeof award.application_links === 'string' + ? JSON.parse(award.application_links) + : award.application_links; + } catch (e) { + console.error('解析 application_links 失敗:', e); + } + } + + if (!applicationLinks) { + return ( +
+ +

暫無應用連結

+
+ ); + } + + return ( + <> + {applicationLinks.production && ( +
+
+ +
+

正式應用

+

APP 連結

+
+
+ +
+ )} + + {applicationLinks.demo && ( +
+
+ +
+

演示版本

+

體驗環境

+
+
+ +
+ )} + + {applicationLinks.github && ( +
+
+ +
+

源碼倉庫

+

GitHub

+
+
+ +
+ )} + + ); + })()} +
+
+
+ + {/* Reports Section */} + + + + + 相關報告 + + 技術文檔和報告資料(僅供預覽) + + +
+ {(() => { + // 解析 documents 資料 + let documents = []; + if (award.documents) { + documents = award.documents; + } else if (award.documents) { + try { + documents = typeof award.documents === 'string' + ? JSON.parse(award.documents) + : award.documents; + } catch (e) { + console.error('解析 documents 失敗:', e); + } + } + + if (!documents || documents.length === 0) { + return ( +
+ +

暫無相關文件

+
+ ); + } + + return documents.map((doc, index) => ( +
+
+ {getFileIcon(doc.type)} +
+

{doc.name || doc.title}

+

{doc.description}

+
+ 大小:{doc.size} + 上傳:{doc.uploadDate || doc.upload_date} + {doc.type} +
+
+
+
+ {doc.previewUrl && ( + + )} + {doc.downloadUrl && ( + + )} + {doc.url && ( + + )} +
+
+ )); + })()} +
+
+
+
+ ) + + const renderCompetitionPhotos = () => ( + + + + + 競賽照片 + + 競賽當天的精彩瞬間 + + +
+ {competitionPhotos.map((photo, index) => ( +
{ + setCurrentPhotoIndex(index) + setShowPhotoGallery(true) + }} + > + {photo.title} +
+ +
+
+

{photo.title}

+
+
+ ))} +
+ +
+ +
+
+
+ ) + + const renderJudgePanel = () => { + if (!competitionJudges || competitionJudges.length === 0) { + return ( + + + + + 評審團 + + 本次競賽的專業評審團隊 + + +
+ +

暫無評審信息

+
+
+
+ ) + + return ( + + + + + 評審團 + + 本次競賽的專業評審團隊 + + +
+ {competitionJudges.map((judge) => ( +
+ + + {judge.name[0]} + +
+

{judge.name}

+

{judge.title}

+
+ {judge.expertise && judge.expertise.slice(0, 2).map((skill) => ( + + {skill} + + ))} +
+
+
+ ))} +
+
+
+
+ } + + const renderJudgeScores = () => ( +
+ {/* Overall Statistics */} + + + + + 評分統計 + + 評審團整體評分概況 + + + {loadingScores ? ( +
+
+

載入評分資料中...

+
+ ) : judgeScores.length === 0 ? ( +
+ +

暫無評分資料

+
+ ) : ( +
+
+
+ {(judgeScores.reduce((sum, score) => sum + score.overallScore, 0) / judgeScores.length).toFixed(1)} +
+
平均分數
+
+
+
+ {Math.max(...judgeScores.map((s) => s.overallScore)).toFixed(1)} +
+
最高分數
+
+
+
+ {Math.min(...judgeScores.map((s) => s.overallScore)).toFixed(1)} +
+
最低分數
+
+
+
{judgeScores.length}
+
評審人數
+
+
+ )} +
+
+ + {/* Individual Judge Scores */} + {loadingScores ? ( +
+
+

載入評審評分中...

+
+ ) : judgeScores.length === 0 ? ( +
+ +

暫無評審評分資料

+
+ ) : ( + judgeScores.map((judgeScore, index) => ( + + +
+ + + {judgeScore.judgeName[0]} + +
+ {judgeScore.judgeName} + {judgeScore.judgeTitle} +
+
+
+ + {judgeScore.overallScore} + /5.0 +
+
評分時間:{judgeScore.submittedAt}
+
+
+
+ + {/* Criteria Scores */} +
+

+ + 評分細項 +

+
+ {judgeScore.criteria.map((criterion, criterionIndex) => ( +
+
+ {criterion.name} + + {criterion.score}/{criterion.maxScore} + +
+ +
+ ))} +
+
+ + {/* Judge Comment */} +
+

+ + 評審評語 +

+
+

{judgeScore.comment}

+
+
+
+
+ )) + )} +
+ ) + + return ( + <> + + + + + + 得獎作品詳情 + + + {competition?.name} - {award.awardName} + + + +
+ + + 獎項概覽 + 競賽照片 + 評審團 + 評分詳情 + + + + {renderAwardOverview()} + + + + {renderCompetitionPhotos()} + + + + {renderJudgePanel()} + + + + {renderJudgeScores()} + + +
+
+
+ + {/* Photo Gallery Modal */} + + +
+ + +
+ {competitionPhotos[currentPhotoIndex]?.title} + + + + + +
+

{competitionPhotos[currentPhotoIndex]?.title}

+

{competitionPhotos[currentPhotoIndex]?.description}

+
+
+ +
+
+ {competitionPhotos.map((_, index) => ( +
+
+ {currentPhotoIndex + 1} / {competitionPhotos.length} +
+
+
+
+
+ + ) +} diff --git a/components/competition/award-detail-dialog-fixed.tsx b/components/competition/award-detail-dialog-fixed.tsx new file mode 100644 index 0000000..737886b --- /dev/null +++ b/components/competition/award-detail-dialog-fixed.tsx @@ -0,0 +1,765 @@ +"use client" + +import { useState, useEffect } from "react" +import { useCompetition } from "@/contexts/competition-context" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Progress } from "@/components/ui/progress" +import { + Target, + Users, + Lightbulb, + Trophy, + Crown, + Award, + Camera, + ImageIcon, + ChevronLeft, + ChevronRight, + X, + Star, + MessageSquare, + BarChart3, + ExternalLink, + Eye, + Link, + FileText, + Download, +} from "lucide-react" +import type { Award as AwardType } from "@/types/competition" + +interface AwardDetailDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + award: AwardType +} + +export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDialogProps) { + const { competitions, judges, getTeamById, getProposalById } = useCompetition() + const [activeTab, setActiveTab] = useState("overview") + const [showPhotoGallery, setShowPhotoGallery] = useState(false) + const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0) + const [competitionJudges, setCompetitionJudges] = useState([]) + const [judgeScores, setJudgeScores] = useState([]) + const [loadingScores, setLoadingScores] = useState(false) + + // 添加調試資訊 + console.log('🏆 AwardDetailDialog 渲染:', { + open, + award: award ? { + id: award.id, + competitionId: award.competitionId, + awardName: award.awardName, + hasCompetitionId: !!award.competitionId + } : null + }); + + const competition = competitions.find((c) => c.id === award.competitionId) + + // 載入競賽評審團資訊 + useEffect(() => { + console.log('🔍 useEffect 觸發:', { open, competitionId: award.competitionId, awardId: award.id }); + + if (open && award.competitionId) { + const loadCompetitionJudges = async (retryCount = 0) => { + try { + console.log('🔍 載入競賽評審團:', award.competitionId, '重試次數:', retryCount); + + const response = await fetch(`/api/competitions/${award.competitionId}/judges?t=${Date.now()}`, { + method: 'GET', + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('📊 評審團API回應:', data); + + if (data.success) { + console.log('✅ 載入評審團成功:', data.data.length, '位'); + setCompetitionJudges(data.data); + } else { + console.error('❌ 載入評審團失敗:', data.message); + setCompetitionJudges([]); + } + } catch (error) { + console.error('❌ 載入評審團錯誤:', error); + if (retryCount < 2) { + console.log('🔄 重試載入評審團...', retryCount + 1); + setTimeout(() => loadCompetitionJudges(retryCount + 1), 1000); + } else { + setCompetitionJudges([]); + } + } + }; + + loadCompetitionJudges(); + } else { + console.log('❌ useEffect 條件不滿足:', { + open, + competitionId: award.competitionId, + hasCompetitionId: !!award.competitionId + }); + } + }, [open, award.competitionId]); + + // 載入評分詳情 + useEffect(() => { + if (open && award.id) { + const loadJudgeScores = async () => { + try { + setLoadingScores(true); + console.log('🔍 載入評分詳情:', award.id); + + const response = await fetch(`/api/awards/${award.id}/scores`); + const data = await response.json(); + + if (data.success) { + console.log('✅ 載入評分詳情成功:', data.data.length, '筆'); + setJudgeScores(data.data); + } else { + console.error('❌ 載入評分詳情失敗:', data.message); + setJudgeScores([]); + } + } catch (error) { + console.error('❌ 載入評分詳情錯誤:', error); + setJudgeScores([]); + } finally { + setLoadingScores(false); + } + }; + + loadJudgeScores(); + } else { + setJudgeScores([]); + } + }, [open, award.id]); + + // Competition photos - empty for production + const getCompetitionPhotos = () => { + return [] + } + + const competitionPhotos = getCompetitionPhotos() + + const getCompetitionTypeIcon = (type: string) => { + switch (type) { + case "individual": + return + case "team": + return + case "proposal": + return + default: + return + } + } + + const getCompetitionTypeText = (type: string) => { + switch (type) { + case "individual": + return "個人賽" + case "team": + return "團隊賽" + case "proposal": + return "提案賽" + default: + return "競賽" + } + } + + const getCompetitionTypeColor = (type: string) => { + switch (type) { + case "individual": + return "bg-blue-100 text-blue-800 border-blue-200" + case "team": + return "bg-green-100 text-green-800 border-green-200" + case "proposal": + return "bg-purple-100 text-purple-800 border-purple-200" + default: + return "bg-gray-100 text-gray-800 border-gray-200" + } + } + + const getFileIcon = (type: string) => { + switch (type.toLowerCase()) { + case "pdf": + return + case "pptx": + case "ppt": + return + case "docx": + case "doc": + return + default: + return + } + } + + const nextPhoto = () => { + setCurrentPhotoIndex((prev) => (prev + 1) % competitionPhotos.length) + } + + const prevPhoto = () => { + setCurrentPhotoIndex((prev) => (prev - 1 + competitionPhotos.length) % competitionPhotos.length) + } + + const handlePreview = (report: any) => { + // Open preview in new window + window.open(report.previewUrl, "_blank") + } + + const renderAwardOverview = () => ( +
+ + +
+
{award.icon}
+
+ {award.awardName} + + {award.appName || award.proposalTitle || award.teamName} + +
+ + {getCompetitionTypeIcon(award.competitionType)} + {getCompetitionTypeText(award.competitionType)} + + + {award.awardName} + +
+
+
+
+ +
+
+
{award.score}
+
評審評分
+
+
+
+ {award.year}年{award.month}月 +
+
獲獎時間
+
+
+
+ {award.teamName || award.creator || "創作者"} +
+
+ {award.competitionType === "team" ? "團隊" : "創作者"} +
+
+
+ +
+

+ + 競賽資訊 +

+
+

+ 競賽名稱: + {award.competitionName || competition?.name || '未知競賽'} +

+

+ 競賽描述: + {award.competitionDescription || competition?.description || '暫無描述'} +

+

+ 競賽期間: + {award.competitionStartDate && award.competitionEndDate + ? `${award.competitionStartDate} ~ ${award.competitionEndDate}` + : competition?.startDate && competition?.endDate + ? `${competition.startDate} ~ ${competition.endDate}` + : '暫無時間資訊' + } +

+

+ 評審團: + {competitionJudges && competitionJudges.length > 0 ? ( + + {competitionJudges.length} 位評審 + + ) : ( + 暫無評審信息 + )} +

+
+
+
+
+ + {/* App Links Section */} + + + + + 應用連結 + + 相關應用和資源連結 + + +
+ {(() => { + // 解析 application_links 資料 + let applicationLinks = null; + if (award.applicationLinks) { + applicationLinks = award.applicationLinks; + } else if (award.application_links) { + try { + applicationLinks = typeof award.application_links === 'string' + ? JSON.parse(award.application_links) + : award.application_links; + } catch (e) { + console.error('解析 application_links 失敗:', e); + } + } + + if (!applicationLinks) { + return ( +
+ +

暫無應用連結

+
+ ); + } + + return ( + <> + {applicationLinks.production && ( +
+
+ +
+

正式應用

+

APP 連結

+
+
+ +
+ )} + + {applicationLinks.demo && ( +
+
+ +
+

演示版本

+

體驗環境

+
+
+ +
+ )} + + {applicationLinks.github && ( +
+
+ +
+

源碼倉庫

+

GitHub

+
+
+ +
+ )} + + ); + })()} +
+
+
+ + {/* Reports Section */} + + + + + 相關報告 + + 技術文檔和報告資料(僅供預覽) + + +
+ {(() => { + // 解析 documents 資料 + let documents = []; + if (award.documents) { + documents = award.documents; + } else if (award.documents) { + try { + documents = typeof award.documents === 'string' + ? JSON.parse(award.documents) + : award.documents; + } catch (e) { + console.error('解析 documents 失敗:', e); + } + } + + if (!documents || documents.length === 0) { + return ( +
+ +

暫無相關文件

+
+ ); + } + + return documents.map((doc, index) => ( +
+
+ {getFileIcon(doc.type)} +
+

{doc.name || doc.title}

+

{doc.description}

+
+ 大小:{doc.size} + 上傳:{doc.uploadDate || doc.upload_date} + {doc.type} +
+
+
+
+ {doc.previewUrl && ( + + )} + {doc.downloadUrl && ( + + )} + {doc.url && ( + + )} +
+
+ )); + })()} +
+
+
+
+ ) + + const renderJudgePanel = () => { + // 確保 competitionJudges 是陣列 + const judges = Array.isArray(competitionJudges) ? competitionJudges : []; + + if (judges.length === 0) { + return ( + + + + + 評審團 + + 本次競賽的專業評審團隊 + + +
+ +

暫無評審信息

+
+
+
+ ) + } + + return ( + + + + + 評審團 + + 本次競賽的專業評審團隊 + + +
+ {judges.map((judge, index) => ( +
+ + + + {judge.name ? judge.name[0] : 'J'} + + +

{judge.name}

+

{judge.title}

+
+ {judge.skills && judge.skills.map((skill, skillIndex) => ( + + {skill} + + ))} +
+
+ ))} +
+
+
+ ) + } + + const renderJudgeScores = () => { + // 確保 judgeScores 是陣列 + const scores = Array.isArray(judgeScores) ? judgeScores : []; + + return ( +
+ {/* Overall Statistics */} + + + + + 評分統計 + + 評審團整體評分概況 + + + {loadingScores ? ( +
+
+

載入評分資料中...

+
+ ) : scores.length === 0 ? ( +
+ +

暫無評分資料

+
+ ) : ( +
+
+
+ {(scores.reduce((sum, score) => sum + score.overallScore, 0) / scores.length).toFixed(1)} +
+
平均分數
+
+
+
+ {Math.max(...scores.map((s) => s.overallScore)).toFixed(1)} +
+
最高分數
+
+
+
+ {Math.min(...scores.map((s) => s.overallScore)).toFixed(1)} +
+
最低分數
+
+
+
{scores.length}
+
評審人數
+
+
+ )} +
+
+ + {/* Individual Judge Scores */} + {loadingScores ? ( +
+
+

載入評審評分中...

+
+ ) : scores.length === 0 ? ( +
+ +

暫無評審評分資料

+
+ ) : ( + scores.map((judgeScore, index) => ( + + +
+ + + {judgeScore.judgeName[0]} + +
+ {judgeScore.judgeName} + {judgeScore.judgeTitle} +
+
+
+ + {judgeScore.overallScore} + /5.0 +
+
評分時間:{judgeScore.submittedAt}
+
+
+
+ + {/* Criteria Scores */} +
+

+ + 評分細項 +

+
+ {judgeScore.criteria.map((criterion, criterionIndex) => ( +
+
+ {criterion.name} + + {criterion.score}/{criterion.maxScore} + +
+ +
+ ))} +
+
+ + {/* Judge Comment */} +
+

+ + 評審評語 +

+
+

{judgeScore.comment}

+
+
+
+
+ )) + )} +
+ ) + } + + const renderCompetitionPhotos = () => ( + + + + + 競賽照片 + + 競賽活動精彩瞬間 + + +
+ +

暫無競賽照片

+
+
+
+ ) + + return ( + <> + + + + + + 得獎作品詳情 + + + {award.competitionName} - {award.awardName} + + +
+ + + 獎項概覽 + 競賽照片 + 評審團 + 評分詳情 + + + + {renderAwardOverview()} + + + + {renderCompetitionPhotos()} + + + + {renderJudgePanel()} + + + + {renderJudgeScores()} + + +
+
+
+ + ) +} \ No newline at end of file diff --git a/components/competition/award-detail-dialog.tsx b/components/competition/award-detail-dialog.tsx index db98a0e..daf1521 100644 --- a/components/competition/award-detail-dialog.tsx +++ b/components/competition/award-detail-dialog.tsx @@ -28,6 +28,7 @@ import { Eye, Link, FileText, + Download, } from "lucide-react" import type { Award as AwardType } from "@/types/competition" @@ -37,28 +38,14 @@ interface AwardDetailDialogProps { award: AwardType } -// Judge scoring data - empty for production -const getJudgeScores = (awardId: string) => { - return [] -} - -// App links and reports data - empty for production -const getAppData = (awardId: string) => { - return { - appUrl: "", - demoUrl: "", - githubUrl: "", - reports: [], - } -} - - export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDialogProps) { const { competitions, judges, getTeamById, getProposalById } = useCompetition() const [activeTab, setActiveTab] = useState("overview") const [showPhotoGallery, setShowPhotoGallery] = useState(false) const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0) const [competitionJudges, setCompetitionJudges] = useState([]) + const [judgeScores, setJudgeScores] = useState([]) + const [loadingScores, setLoadingScores] = useState(false) // 添加調試資訊 console.log('🏆 AwardDetailDialog 渲染:', { @@ -72,8 +59,6 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial }); const competition = competitions.find((c) => c.id === award.competitionId) - const judgeScores = getJudgeScores(award.id) - const appData = getAppData(award.id) // 載入競賽評審團資訊 useEffect(() => { @@ -83,14 +68,8 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial const loadCompetitionJudges = async (retryCount = 0) => { try { console.log('🔍 載入競賽評審團:', award.competitionId, '重試次數:', retryCount); - console.log('🏆 獎項資料:', award); - // 添加時間戳防止快取,並設置快取控制標頭 - const timestamp = Date.now(); - const apiUrl = `/api/competitions/${award.competitionId}/judges?t=${timestamp}`; - console.log('🌐 API URL:', apiUrl); - - const response = await fetch(apiUrl, { + const response = await fetch(`/api/competitions/${award.competitionId}/judges?t=${Date.now()}`, { method: 'GET', headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', @@ -99,59 +78,73 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial } }); - console.log('📡 API 回應狀態:', response.status); - console.log('📡 API 回應標頭:', Object.fromEntries(response.headers.entries())); - if (!response.ok) { - const errorText = await response.text(); - console.error('❌ API 錯誤回應:', errorText); - throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); + throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); - console.log('📄 API 回應資料:', JSON.stringify(data, null, 2)); + console.log('📊 評審團API回應:', data); - if (data.success && data.data && data.data.judges) { - console.log('✅ 獲取到評審團:', data.data.judges.length, '位'); - console.log('👥 評審團詳細資料:', data.data.judges); + if (data.success) { + console.log('✅ 載入評審團成功:', data.data.judges.length, '位'); setCompetitionJudges(data.data.judges); } else { - console.error('❌ 獲取評審團失敗:', data.message || '未知錯誤'); - console.error('❌ 完整錯誤資料:', data); - - // 如果沒有評審資料且是第一次嘗試,重試一次 - if (retryCount === 0) { - console.log('🔄 重試載入評審團...'); - setTimeout(() => loadCompetitionJudges(1), 1000); - } else { - setCompetitionJudges([]); - } + console.error('❌ 載入評審團失敗:', data.message); + setCompetitionJudges([]); } } catch (error) { - console.error('❌ 載入評審團失敗:', error); - - // 如果是網路錯誤且是第一次嘗試,重試一次 - if (retryCount === 0) { - console.log('🔄 網路錯誤,重試載入評審團...'); - setTimeout(() => loadCompetitionJudges(1), 2000); + console.error('❌ 載入評審團錯誤:', error); + if (retryCount < 2) { + console.log('🔄 重試載入評審團...', retryCount + 1); + setTimeout(() => loadCompetitionJudges(retryCount + 1), 1000); } else { setCompetitionJudges([]); } } }; - // 清空之前的評審資料,確保重新載入 - setCompetitionJudges([]); loadCompetitionJudges(); } else { - console.log('❌ useEffect 條件不滿足:', { - open, + console.log('❌ useEffect 條件不滿足:', { + open, competitionId: award.competitionId, - hasCompetitionId: !!award.competitionId + hasCompetitionId: !!award.competitionId }); } }, [open, award.competitionId]); + // 載入評分詳情 + useEffect(() => { + if (open && award.id) { + const loadJudgeScores = async () => { + try { + setLoadingScores(true); + console.log('🔍 載入評分詳情:', award.id); + + const response = await fetch(`/api/awards/${award.id}/scores`); + const data = await response.json(); + + if (data.success) { + console.log('✅ 載入評分詳情成功:', data.data.length, '筆'); + setJudgeScores(data.data); + } else { + console.error('❌ 載入評分詳情失敗:', data.message); + setJudgeScores([]); + } + } catch (error) { + console.error('❌ 載入評分詳情錯誤:', error); + setJudgeScores([]); + } finally { + setLoadingScores(false); + } + }; + + loadJudgeScores(); + } else { + setJudgeScores([]); + } + }, [open, award.id]); + // Competition photos - empty for production const getCompetitionPhotos = () => { return [] @@ -235,7 +228,10 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
{award.awardName} - {award.appName || award.proposalTitle || award.teamName} + {award.competitionType === "team" + ? (award.teamName || award.appName || "團隊名稱") + : (award.appName || award.proposalTitle || award.teamName) + }
@@ -248,10 +244,10 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial award.awardType === "gold" ? "bg-yellow-100 text-yellow-800 border-yellow-200" : award.awardType === "silver" - ? "bg-gray-100 text-gray-800 border-gray-200" - : award.awardType === "bronze" - ? "bg-orange-100 text-orange-800 border-orange-200" - : "bg-red-100 text-red-800 border-red-200" + ? "bg-gray-100 text-gray-800 border-gray-200" + : award.awardType === "bronze" + ? "bg-orange-100 text-orange-800 border-orange-200" + : "bg-purple-100 text-purple-800 border-purple-200" }`} > {award.awardName} @@ -261,75 +257,65 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
-
-
-
- {award.awardType === "popular" && award.competitionType === "team" - ? `${award.score}` - : award.awardType === "popular" - ? `${award.score}` - : award.score} -
-
- {award.competitionType === "proposal" - ? "評審評分" - : award.awardType === "popular" - ? award.competitionType === "team" - ? "人氣指數" - : "收藏數" - : "評審評分"} -
+
+
+
{award.score}
+
評審評分
-
-
+
+
{award.year}年{award.month}月
-
獲獎時間
+
獲獎時間
-
-
{award.creator}
-
- {award.competitionType === "team" - ? "團隊" - : award.competitionType === "proposal" - ? "提案團隊" - : "創作者"} +
+
+ {award.competitionType === "team" + ? (award.appName || award.teamName || "應用名稱") + : (award.teamName || award.creator || "創作者") + } +
+
+ {award.competitionType === "team" ? "應用名稱" : "創作者"}
- {competition && ( -
-

- - 競賽資訊 -

-
-

- 競賽名稱: - {competition.name} -

-

- 競賽描述: - {competition.description} -

-

- 競賽期間: - {competition.startDate} ~ {competition.endDate} -

-

- 評審團: - {competitionJudges && competitionJudges.length > 0 ? ( - - {competitionJudges.length} 位評審 - - ) : ( - 暫無評審信息 - )} -

-
+
+

+ + 競賽資訊 +

+
+

+ 競賽名稱: + {award.competitionName || competition?.name || '未知競賽'} +

+

+ 競賽描述: + {award.competitionDescription || competition?.description || '暫無描述'} +

+

+ 競賽期間: + {award.competitionStartDate && award.competitionEndDate + ? `${new Date(award.competitionStartDate).toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '/')} ~ ${new Date(award.competitionEndDate).toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '/')}` + : competition?.startDate && competition?.endDate + ? `${new Date(competition.startDate).toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '/')} ~ ${new Date(competition.endDate).toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '/')}` + : '暫無時間資訊' + } +

+

+ 評審團: + {competitionJudges && competitionJudges.length > 0 ? ( + + {competitionJudges.length} 位評審 + + ) : ( + 暫無評審信息 + )} +

- )} +
@@ -344,65 +330,94 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
- {appData.appUrl && ( -
-
- -
-

正式應用

-

APP 連結

-
-
- -
- )} + {(() => { + // 解析 application_links 資料 + let applicationLinks = null; + if (award.applicationLinks) { + applicationLinks = award.applicationLinks; + } else if (award.application_links) { + try { + applicationLinks = typeof award.application_links === 'string' + ? JSON.parse(award.application_links) + : award.application_links; + } catch (e) { + console.error('解析 application_links 失敗:', e); + } + } - {appData.demoUrl && ( -
-
- -
-

演示版本

-

體驗環境

+ if (!applicationLinks) { + return ( +
+ +

暫無應用連結

-
- -
- )} + ); + } - {appData.githubUrl && ( -
-
- -
-

源碼倉庫

-

GitHub

-
-
- -
- )} + return ( + <> + {applicationLinks.production && ( +
+
+ +
+

正式應用

+

APP 連結

+
+
+ +
+ )} + + {applicationLinks.demo && ( +
+
+ +
+

演示版本

+

體驗環境

+
+
+ +
+ )} + + {applicationLinks.github && ( +
+
+ +
+

源碼倉庫

+

GitHub

+
+
+ +
+ )} + + ); + })()}
@@ -418,95 +433,96 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
- {appData.reports.map((report) => ( -
-
- {getFileIcon(report.type)} -
-

{report.name}

-

{report.description}

-
- 大小:{report.size} - 上傳:{report.uploadDate} - {report.type} + {(() => { + // 解析 documents 資料 + let documents = []; + if (award.documents) { + documents = award.documents; + } else if (award.documents) { + try { + documents = typeof award.documents === 'string' + ? JSON.parse(award.documents) + : award.documents; + } catch (e) { + console.error('解析 documents 失敗:', e); + } + } + + if (!documents || documents.length === 0) { + return ( +
+ +

暫無相關文件

+
+ ); + } + + return documents.map((doc, index) => ( +
+
+ {getFileIcon(doc.type)} +
+

{doc.name || doc.title}

+

{doc.description}

+
+ 大小:{doc.size} + 上傳:{doc.uploadDate || doc.upload_date} + {doc.type} +
+
+ {doc.previewUrl && ( + + )} + {doc.downloadUrl && ( + + )} + {doc.url && ( + + )} +
-
- -
-
- ))} + )); + })()}
) - const renderCompetitionPhotos = () => ( - - - - - 競賽照片 - - 競賽當天的精彩瞬間 - - -
- {competitionPhotos.map((photo, index) => ( -
{ - setCurrentPhotoIndex(index) - setShowPhotoGallery(true) - }} - > - {photo.title} -
- -
-
-

{photo.title}

-
-
- ))} -
- -
- -
-
-
- ) - const renderJudgePanel = () => { - if (!competitionJudges || competitionJudges.length === 0) { + // 確保 competitionJudges 是陣列 + const judges = Array.isArray(competitionJudges) ? competitionJudges : []; + + if (judges.length === 0) { return ( @@ -536,23 +552,23 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial 本次競賽的專業評審團隊 -
- {competitionJudges.map((judge) => ( -
- +
+ {judges.map((judge, index) => ( +
+ - {judge.name[0]} + + {judge.name ? judge.name[0] : 'J'} + -
-

{judge.name}

-

{judge.title}

-
- {judge.expertise && judge.expertise.slice(0, 2).map((skill) => ( - - {skill} - - ))} -
+

{judge.name}

+

{judge.title}

+
+ {judge.expertise && judge.expertise.map((skill, skillIndex) => ( + + {skill} + + ))}
))} @@ -562,104 +578,151 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial ) } - const renderJudgeScores = () => ( -
- {/* Overall Statistics */} - - - - - 評分統計 - - 評審團整體評分概況 - - -
-
-
- {(judgeScores.reduce((sum, score) => sum + score.overallScore, 0) / judgeScores.length).toFixed(1)} -
-
平均分數
-
-
-
- {Math.max(...judgeScores.map((s) => s.overallScore)).toFixed(1)} -
-
最高分數
-
-
-
- {Math.min(...judgeScores.map((s) => s.overallScore)).toFixed(1)} -
-
最低分數
-
-
-
{judgeScores.length}
-
評審人數
-
-
-
-
- - {/* Individual Judge Scores */} - {judgeScores.map((judgeScore, index) => ( - + const renderJudgeScores = () => { + // 確保 judgeScores 是陣列 + const scores = Array.isArray(judgeScores) ? judgeScores : []; + + return ( +
+ {/* Overall Statistics */} + -
- - - {judgeScore.judgeName[0]} - -
- {judgeScore.judgeName} - {judgeScore.judgeTitle} -
-
-
- - {judgeScore.overallScore} - /5.0 -
-
評分時間:{judgeScore.submittedAt}
-
-
+ + + 評分統計 + + 評審團整體評分概況
- {/* Criteria Scores */} -
-

- - 評分細項 -

-
- {judgeScore.criteria.map((criterion, criterionIndex) => ( -
-
- {criterion.name} - - {criterion.score}/{criterion.maxScore} - -
- + {loadingScores ? ( +
+
+

載入評分資料中...

+
+ ) : scores.length === 0 ? ( +
+ +

暫無評分資料

+
+ ) : ( +
+
+
+ {scores.length > 0 ? (scores.reduce((sum, score) => sum + Number(score.overallScore), 0) / scores.length).toFixed(1) : '0.0'}
- ))} +
平均分數
+
+
+
+ {scores.length > 0 ? Math.max(...scores.map((s) => Number(s.overallScore))).toFixed(1) : '0.0'} +
+
最高分數
+
+
+
+ {scores.length > 0 ? Math.min(...scores.map((s) => Number(s.overallScore))).toFixed(1) : '0.0'} +
+
最低分數
+
+
+
{scores.length}
+
評審人數
+
-
- - {/* Judge Comment */} -
-

- - 評審評語 -

-
-

{judgeScore.comment}

-
-
+ )} - ))} -
+ + {/* Individual Judge Scores */} + {loadingScores ? ( +
+
+

載入評審評分中...

+
+ ) : scores.length === 0 ? ( +
+ +

暫無評審評分資料

+
+ ) : ( + scores.map((judgeScore, index) => ( + + +
+ + + {judgeScore.judgeName[0]} + +
+ {judgeScore.judgeName} + {judgeScore.judgeTitle} +
+
+
+ + {Number(judgeScore.overallScore).toFixed(1)} + /100 +
+
評分時間:{new Date(judgeScore.submittedAt).toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '/')}
+
+
+
+ + {/* Criteria Scores */} +
+

+ + 評分細項 +

+
+ {judgeScore.criteria.map((criterion, criterionIndex) => ( +
+
+ {criterion.name} + + {criterion.score}/{criterion.maxScore} + +
+ +
+ ))} +
+
+ + {/* Judge Comment */} +
+

+ + 評審評語 +

+
+

{judgeScore.comment}

+
+
+
+
+ )) + )} +
+ ) + } + + const renderCompetitionPhotos = () => ( + + + + + 競賽照片 + + 競賽活動精彩瞬間 + + +
+ +

暫無競賽照片

+
+
+
) return ( @@ -672,10 +735,9 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial 得獎作品詳情 - {competition?.name} - {award.awardName} + {award.competitionName} - {award.awardName} -
@@ -704,70 +766,6 @@ export function AwardDetailDialog({ open, onOpenChange, award }: AwardDetailDial
- - {/* Photo Gallery Modal */} - - -
- - -
- {competitionPhotos[currentPhotoIndex]?.title} - - - - - -
-

{competitionPhotos[currentPhotoIndex]?.title}

-

{competitionPhotos[currentPhotoIndex]?.description}

-
-
- -
-
- {competitionPhotos.map((_, index) => ( -
-
- {currentPhotoIndex + 1} / {competitionPhotos.length} -
-
-
-
-
) -} +} \ No newline at end of file diff --git a/contexts/competition-context.tsx b/contexts/competition-context.tsx index dbbab74..131b32f 100644 --- a/contexts/competition-context.tsx +++ b/contexts/competition-context.tsx @@ -67,6 +67,7 @@ interface CompetitionContextType { addAward: (award: Omit) => void getAwardsByYear: (year: number) => Award[] getAwardsByMonth: (year: number, month: number) => Award[] + getAvailableYears: () => number[] // Rankings getCompetitionRankings: (competitionId?: string) => Array<{ @@ -527,11 +528,13 @@ export function CompetitionProvider({ children }: { children: ReactNode }) { const loadAwards = async () => { setLoadingAwards(true) try { + console.log('🔍 開始載入前台獎項數據...') const response = await fetch('/api/admin/awards') const data = await response.json() if (data.success) { console.log('📊 載入獎項數據:', data.data.length, '個') + console.log('📋 獎項數據樣本:', data.data.slice(0, 2)) setAwards(data.data) } else { console.error('載入獎項失敗:', data.message) @@ -572,6 +575,12 @@ export function CompetitionProvider({ children }: { children: ReactNode }) { return awards.filter((award) => award.year === year) } + // 獲取所有可用的年份 + const getAvailableYears = (): number[] => { + const years = [...new Set(awards.map(award => award.year))].sort((a, b) => b - a) + return years + } + const getAwardsByMonth = (year: number, month: number): Award[] => { return awards.filter((award) => award.year === year && award.month === month) } @@ -836,6 +845,7 @@ export function CompetitionProvider({ children }: { children: ReactNode }) { addAward, getAwardsByYear, getAwardsByMonth, + getAvailableYears, getCompetitionRankings, getAwardsByCompetitionType, getAwardsByCategory, diff --git a/lib/services/database-service.ts b/lib/services/database-service.ts index e1a02cd..2cbcf3e 100644 --- a/lib/services/database-service.ts +++ b/lib/services/database-service.ts @@ -4572,14 +4572,19 @@ export class AwardService extends DatabaseServiceBase { const simpleResult = await db.query(simpleSql); console.log('✅ 簡單查詢結果:', simpleResult?.length || 0, '個獎項'); - // 再執行 JOIN 查詢 + // 再執行 JOIN 查詢,包含團隊資訊 const sql = ` SELECT a.*, c.name as competition_name, - c.type as competition_type + c.type as competition_type, + c.description as competition_description, + c.start_date as competition_start_date, + c.end_date as competition_end_date, + t.name as team_name_from_teams FROM awards a LEFT JOIN competitions c ON a.competition_id = c.id + LEFT JOIN teams t ON a.team_id = t.id ORDER BY a.created_at DESC `; console.log('📝 執行JOIN查詢:', sql); @@ -4605,7 +4610,21 @@ export class AwardService extends DatabaseServiceBase { // 根據競賽獲取獎項 static async getAwardsByCompetition(competitionId: string): Promise { - const sql = 'SELECT * FROM awards WHERE competition_id = ? ORDER BY created_at DESC'; + const sql = ` + SELECT + a.*, + c.name as competition_name, + c.type as competition_type, + c.description as competition_description, + c.start_date as competition_start_date, + c.end_date as competition_end_date, + t.name as team_name_from_teams + FROM awards a + LEFT JOIN competitions c ON a.competition_id = c.id + LEFT JOIN teams t ON a.team_id = t.id + WHERE a.competition_id = ? + ORDER BY a.created_at DESC + `; return await db.query(sql, [competitionId]); } diff --git a/types/competition.ts b/types/competition.ts index 925307b..fe97683 100644 --- a/types/competition.ts +++ b/types/competition.ts @@ -83,6 +83,8 @@ export interface Award { appId?: string // 個人賽和團隊賽使用 teamId?: string // 團隊賽和提案賽使用 appName?: string + teamName?: string + proposalTitle?: string creator: string awardType: "gold" | "silver" | "bronze" | "popular" | "innovation" | "technical" | "custom" awardName: string @@ -91,7 +93,7 @@ export interface Award { month: number icon: string customAwardTypeId?: string // 如果是自定義獎項類型 - competitionType: "individual" | "team" // 競賽類型 + competitionType: "individual" | "team" | "proposal" // 競賽類型 rank: number // 0 for non-ranking awards, 1-3 for top 3 category: "innovation" | "technical" | "practical" | "popular" | "teamwork" | "solution" | "creativity" }