Files
ai-showcase-platform/components/competition/award-detail-dialog.tsx

1001 lines
39 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<any[]>([])
const [judgeScores, setJudgeScores] = useState<any[]>([])
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.judges.length, '位');
setCompetitionJudges(data.data.judges);
} 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 <Target className="w-4 h-4" />
case "team":
return <Users className="w-4 h-4" />
case "proposal":
return <Lightbulb className="w-4 h-4" />
default:
return <Trophy className="w-4 h-4" />
}
}
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 <FileText className="w-5 h-5 text-red-500" />
case "pptx":
case "ppt":
return <FileText className="w-5 h-5 text-orange-500" />
case "docx":
case "doc":
return <FileText className="w-5 h-5 text-blue-500" />
default:
return <FileText className="w-5 h-5 text-gray-500" />
}
}
const nextPhoto = () => {
setCurrentPhotoIndex((prev) => {
// 獲取當前照片數組
let photos = [];
if (award.photos) {
photos = award.photos;
} else if (award.photos) {
try {
photos = typeof award.photos === 'string'
? JSON.parse(award.photos)
: award.photos;
} catch (e) {
console.warn('解析 photos JSON 失敗:', e);
photos = [];
}
}
if (photos.length === 0) return 0;
return (prev + 1) % photos.length;
})
}
const prevPhoto = () => {
setCurrentPhotoIndex((prev) => {
// 獲取當前照片數組
let photos = [];
if (award.photos) {
photos = award.photos;
} else if (award.photos) {
try {
photos = typeof award.photos === 'string'
? JSON.parse(award.photos)
: award.photos;
} catch (e) {
console.warn('解析 photos JSON 失敗:', e);
photos = [];
}
}
if (photos.length === 0) return 0;
return (prev - 1 + photos.length) % photos.length;
})
}
const handlePreview = (report: any) => {
// Open preview in new window
window.open(report.previewUrl, "_blank")
}
const renderAwardOverview = () => (
<div className="space-y-6">
<Card>
<CardHeader>
<div className="flex items-center space-x-4">
<div className="text-6xl">{award.icon}</div>
<div className="flex-1">
<CardTitle className="text-2xl">{award.awardName}</CardTitle>
<CardDescription className="text-lg">
{award.competitionType === "team"
? (award.teamName || award.appName || "團隊名稱")
: (award.appName || award.proposalTitle || award.teamName)
}
</CardDescription>
<div className="flex items-center space-x-4 mt-2">
<Badge variant="outline" className={getCompetitionTypeColor(award.competitionType)}>
{getCompetitionTypeIcon(award.competitionType)}
<span className="ml-1">{getCompetitionTypeText(award.competitionType)}</span>
</Badge>
<Badge
variant="secondary"
className={`${
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-purple-100 text-purple-800 border-purple-200"
}`}
>
{award.awardName}
</Badge>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-purple-50 rounded-lg">
<div className="text-3xl font-bold text-purple-600">{award.score}</div>
<div className="text-sm text-purple-600"></div>
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg">
<div className="text-3xl font-bold text-blue-600">
{award.year}{award.month}
</div>
<div className="text-sm text-blue-600"></div>
</div>
<div className="text-center p-4 bg-green-50 rounded-lg">
<div className="text-3xl font-bold text-green-600">
{award.competitionType === "team"
? (award.appName || award.teamName || "應用名稱")
: (award.teamName || award.creator || "創作者")
}
</div>
<div className="text-sm text-green-600">
{award.competitionType === "team" ? "應用名稱" : "創作者"}
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
<h4 className="font-semibold text-blue-900 mb-2 flex items-center">
<Trophy className="w-5 h-5 mr-2" />
</h4>
<div className="text-blue-800">
<p className="mb-2">
<strong></strong>
{award.competitionName || competition?.name || '未知競賽'}
</p>
<p className="mb-2">
<strong></strong>
{award.competitionDescription || competition?.description || '暫無描述'}
</p>
<p className="mb-2">
<strong></strong>
{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, '/')}`
: '暫無時間資訊'
}
</p>
<p>
<strong></strong>
{competitionJudges && competitionJudges.length > 0 ? (
<span className="text-green-700">
{competitionJudges.length}
</span>
) : (
<span className="text-gray-500"></span>
)}
</p>
</div>
</div>
</CardContent>
</Card>
{/* App Links Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Link className="w-5 h-5 text-green-500" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{(() => {
// 解析 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 (
<div className="col-span-full text-center py-8 text-gray-500">
<Link className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
);
}
return (
<>
{applicationLinks.production && (
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center space-x-3">
<ExternalLink className="w-5 h-5 text-green-600" />
<div>
<p className="font-medium text-green-800"></p>
<p className="text-xs text-green-600">APP </p>
</div>
</div>
<Button
size="sm"
variant="outline"
className="border-green-300 text-green-700 hover:bg-green-100 bg-transparent"
onClick={() => window.open(applicationLinks.production, "_blank")}
>
</Button>
</div>
)}
{applicationLinks.demo && (
<div className="flex items-center justify-between p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center space-x-3">
<Eye className="w-5 h-5 text-blue-600" />
<div>
<p className="font-medium text-blue-800"></p>
<p className="text-xs text-blue-600"></p>
</div>
</div>
<Button
size="sm"
variant="outline"
className="border-blue-300 text-blue-700 hover:bg-blue-100 bg-transparent"
onClick={() => window.open(applicationLinks.demo, "_blank")}
>
</Button>
</div>
)}
{applicationLinks.github && (
<div className="flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center space-x-3">
<FileText className="w-5 h-5 text-gray-600" />
<div>
<p className="font-medium text-gray-800"></p>
<p className="text-xs text-gray-600">GitHub</p>
</div>
</div>
<Button
size="sm"
variant="outline"
className="border-gray-300 text-gray-700 hover:bg-gray-100 bg-transparent"
onClick={() => window.open(applicationLinks.github, "_blank")}
>
</Button>
</div>
)}
</>
);
})()}
</div>
</CardContent>
</Card>
{/* Reports Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<FileText className="w-5 h-5 text-purple-500" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{(() => {
// 解析 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 (
<div className="text-center py-8 text-gray-500">
<FileText className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
);
}
return documents.map((doc, index) => (
<div
key={doc.id || index}
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center space-x-4">
{getFileIcon(doc.type)}
<div className="flex-1">
<h4 className="font-medium text-gray-900">{doc.name || doc.title}</h4>
<p className="text-sm text-gray-600">{doc.description}</p>
<div className="flex items-center space-x-4 mt-1 text-xs text-gray-500">
<span>{doc.size}</span>
<span>{doc.uploadDate || doc.upload_date}</span>
<span className="uppercase">{doc.type}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{doc.previewUrl && (
<Button
size="sm"
variant="outline"
onClick={() => window.open(doc.previewUrl, "_blank")}
className="text-blue-600 border-blue-300 hover:bg-blue-50"
>
<Eye className="w-4 h-4 mr-1" />
</Button>
)}
{doc.downloadUrl && (
<Button
size="sm"
variant="outline"
onClick={() => window.open(doc.downloadUrl, "_blank")}
className="text-purple-600 border-purple-300 hover:bg-purple-50"
>
<Download className="w-4 h-4 mr-1" />
</Button>
)}
{doc.url && (
<Button
size="sm"
variant="outline"
onClick={() => window.open(doc.url, "_blank")}
className="text-purple-600 border-purple-300 hover:bg-purple-50"
>
<Download className="w-4 h-4 mr-1" />
</Button>
)}
</div>
</div>
));
})()}
</div>
</CardContent>
</Card>
</div>
)
const renderJudgePanel = () => {
// 確保 competitionJudges 是陣列
const judges = Array.isArray(competitionJudges) ? competitionJudges : [];
if (judges.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Crown className="w-5 h-5 text-purple-500" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-8 text-gray-500">
<Crown className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<p></p>
</div>
</CardContent>
</Card>
)
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Crown className="w-5 h-5 text-purple-500" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{judges.map((judge, index) => (
<div key={judge.id || index} className="text-center">
<Avatar className="w-16 h-16 mx-auto mb-3">
<AvatarImage src={judge.avatar} />
<AvatarFallback className="bg-purple-100 text-purple-700 text-lg">
{judge.name ? judge.name[0] : 'J'}
</AvatarFallback>
</Avatar>
<h4 className="font-semibold text-gray-900 mb-1">{judge.name}</h4>
<p className="text-sm text-gray-600 mb-2">{judge.title}</p>
<div className="flex flex-wrap justify-center gap-1">
{judge.expertise && judge.expertise.map((skill, skillIndex) => (
<Badge key={skillIndex} variant="outline" className="text-xs">
{skill}
</Badge>
))}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)
}
const renderJudgeScores = () => {
// 確保 judgeScores 是陣列
const scores = Array.isArray(judgeScores) ? judgeScores : [];
return (
<div className="space-y-6">
{/* Overall Statistics */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<BarChart3 className="w-5 h-5 text-blue-500" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{loadingScores ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">...</p>
</div>
) : scores.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<BarChart3 className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-3 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">
{scores.length > 0 ? (scores.reduce((sum, score) => sum + Number(score.overallScore), 0) / scores.length).toFixed(1) : '0.0'}
</div>
<div className="text-sm text-blue-600"></div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">
{scores.length > 0 ? Math.max(...scores.map((s) => Number(s.overallScore))).toFixed(1) : '0.0'}
</div>
<div className="text-sm text-green-600"></div>
</div>
<div className="text-center p-3 bg-orange-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600">
{scores.length > 0 ? Math.min(...scores.map((s) => Number(s.overallScore))).toFixed(1) : '0.0'}
</div>
<div className="text-sm text-orange-600"></div>
</div>
<div className="text-center p-3 bg-purple-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">{scores.length}</div>
<div className="text-sm text-purple-600"></div>
</div>
</div>
)}
</CardContent>
</Card>
{/* Individual Judge Scores */}
{loadingScores ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">...</p>
</div>
) : scores.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Users className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
) : (
scores.map((judgeScore, index) => (
<Card key={judgeScore.judgeId}>
<CardHeader>
<div className="flex items-center space-x-4">
<Avatar className="w-12 h-12">
<AvatarImage src={judgeScore.judgeAvatar} />
<AvatarFallback className="bg-blue-100 text-blue-700">{judgeScore.judgeName[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<CardTitle className="text-lg">{judgeScore.judgeName}</CardTitle>
<CardDescription>{judgeScore.judgeTitle}</CardDescription>
</div>
<div className="text-right">
<div className="flex items-center space-x-1">
<Star className="w-5 h-5 text-yellow-500" />
<span className="text-2xl font-bold text-gray-900">{Number(judgeScore.overallScore).toFixed(1)}</span>
<span className="text-gray-500">/100</span>
</div>
<div className="text-xs text-gray-500 mt-1">{new Date(judgeScore.submittedAt).toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '/')}</div>
</div>
</div>
</CardHeader>
<CardContent>
{/* Criteria Scores */}
<div className="space-y-4 mb-6">
<h4 className="font-semibold text-gray-900 flex items-center">
<BarChart3 className="w-4 h-4 mr-2" />
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{judgeScore.criteria.map((criterion, criterionIndex) => (
<div key={criterionIndex} className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700">{criterion.name}</span>
<span className="text-sm text-gray-600">
{criterion.score}/{criterion.maxScore}
</span>
</div>
<Progress value={(criterion.score / criterion.maxScore) * 100} className="h-2" />
</div>
))}
</div>
</div>
{/* Judge Comment */}
<div className="space-y-3">
<h4 className="font-semibold text-gray-900 flex items-center">
<MessageSquare className="w-4 h-4 mr-2" />
</h4>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<p className="text-gray-700 leading-relaxed">{judgeScore.comment}</p>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
)
}
const renderCompetitionPhotos = () => {
// 解析 photos 資料
let photos = [];
if (award.photos) {
photos = award.photos;
} else if (award.photos) {
try {
photos = typeof award.photos === 'string'
? JSON.parse(award.photos)
: award.photos;
} catch (e) {
console.error('解析 photos 失敗:', e);
}
}
console.log('🖼️ 競賽照片資料:', {
hasPhotos: !!photos,
photosType: typeof photos,
photosLength: photos?.length,
photosData: photos
});
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Camera className="w-5 h-5 text-blue-500" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{!photos || !Array.isArray(photos) || photos.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<ImageIcon className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<p></p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{photos.map((photo: any, index: number) => {
console.log('📸 處理照片:', { index, photo });
return (
<div key={photo.id || photo.url || index} className="space-y-2">
<div className="aspect-video bg-gray-100 rounded-lg border overflow-hidden cursor-pointer hover:shadow-md transition-shadow"
onClick={() => {
setCurrentPhotoIndex(index);
setShowPhotoGallery(true);
}}>
{photo.url ? (
<img
src={photo.url}
alt={photo.caption || photo.name || "競賽照片"}
className="w-full h-full object-cover"
onError={(e) => {
console.log('❌ 圖片載入失敗:', photo.url);
e.currentTarget.style.display = 'none';
e.currentTarget.nextElementSibling?.classList.remove('hidden');
}}
onLoad={() => {
console.log('✅ 圖片載入成功:', photo.url);
}}
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center text-2xl">
🖼
</div>
)}
<div className="w-full h-full bg-gray-200 flex items-center justify-center text-2xl hidden">
🖼
</div>
</div>
{(photo.caption || photo.name) && (
<p className="text-xs text-gray-600 text-center line-clamp-2">
{photo.caption || photo.name}
</p>
)}
</div>
);
})}
</div>
)}
</CardContent>
</Card>
);
}
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl flex items-center space-x-2">
<Award className="w-6 h-6 text-purple-500" />
<span></span>
</DialogTitle>
<DialogDescription>
{award.competitionName} - {award.awardName}
</DialogDescription>
</DialogHeader>
<div className="mt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="judges"></TabsTrigger>
<TabsTrigger value="scores"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
{renderAwardOverview()}
</TabsContent>
<TabsContent value="photos" className="space-y-6">
{renderCompetitionPhotos()}
</TabsContent>
<TabsContent value="judges" className="space-y-6">
{renderJudgePanel()}
</TabsContent>
<TabsContent value="scores" className="space-y-6">
{renderJudgeScores()}
</TabsContent>
</Tabs>
</div>
</DialogContent>
</Dialog>
{/* Photo Gallery Modal */}
{showPhotoGallery && (() => {
// 解析 photos 資料
let photos = [];
if (award.photos) {
photos = award.photos;
} else if (award.photos) {
try {
photos = typeof award.photos === 'string'
? JSON.parse(award.photos)
: award.photos;
} catch (e) {
console.error('解析 photos 失敗:', e);
}
}
if (!photos || !Array.isArray(photos) || photos.length === 0) {
return null;
}
const currentPhoto = photos[isNaN(currentPhotoIndex) ? 0 : currentPhotoIndex];
return (
<Dialog open={showPhotoGallery} onOpenChange={setShowPhotoGallery}>
<DialogContent className="max-w-4xl max-h-[90vh] p-0">
<DialogHeader className="sr-only">
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="relative">
{/* Close Button */}
<Button
variant="ghost"
size="sm"
className="absolute top-4 right-4 z-10 bg-black/50 text-white hover:bg-black/70"
onClick={() => setShowPhotoGallery(false)}
>
<X className="w-4 h-4" />
</Button>
{/* Main Photo */}
<div className="relative aspect-video bg-black">
{currentPhoto?.url ? (
<img
src={currentPhoto.url}
alt={currentPhoto.caption || currentPhoto.name || "競賽照片"}
className="w-full h-full object-contain"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-white text-6xl">
🖼
</div>
)}
{/* Navigation Arrows */}
{photos.length > 1 && (
<>
<Button
variant="ghost"
size="sm"
className="absolute left-4 top-1/2 transform -translate-y-1/2 z-10 bg-black/50 text-white hover:bg-black/70"
onClick={prevPhoto}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="absolute right-4 top-1/2 transform -translate-y-1/2 z-10 bg-black/50 text-white hover:bg-black/70"
onClick={nextPhoto}
>
<ChevronRight className="w-4 h-4" />
</Button>
</>
)}
{/* Photo Counter */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/50 text-white px-3 py-1 rounded-full text-sm">
{isNaN(currentPhotoIndex) ? 1 : currentPhotoIndex + 1} / {photos.length}
</div>
</div>
{/* Photo Caption */}
{(currentPhoto?.caption || currentPhoto?.name) && (
<div className="p-4 bg-white border-t">
<p className="text-center text-gray-700">
{currentPhoto.caption || currentPhoto.name}
</p>
</div>
)}
{/* Thumbnail Strip */}
{photos.length > 1 && (
<div className="p-4 bg-gray-50 border-t">
<div className="flex space-x-2 overflow-x-auto">
{photos.map((photo: any, index: number) => (
<button
key={photo.id || photo.url || index}
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 ${
index === currentPhotoIndex
? 'border-blue-500'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setCurrentPhotoIndex(index)}
>
{photo.url ? (
<img
src={photo.url}
alt={photo.caption || photo.name || "競賽照片"}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center text-lg">
🖼
</div>
)}
</button>
))}
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
})()}
</>
)
}