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

765 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.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 <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) => (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 = () => (
<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.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.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">
<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
? `${award.competitionStartDate} ~ ${award.competitionEndDate}`
: competition?.startDate && competition?.endDate
? `${competition.startDate} ~ ${competition.endDate}`
: '暫無時間資訊'
}
</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.skills && judge.skills.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.reduce((sum, score) => sum + score.overallScore, 0) / scores.length).toFixed(1)}
</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">
{Math.max(...scores.map((s) => s.overallScore)).toFixed(1)}
</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">
{Math.min(...scores.map((s) => s.overallScore)).toFixed(1)}
</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">{judgeScore.overallScore}</span>
<span className="text-gray-500">/5.0</span>
</div>
<div className="text-xs text-gray-500 mt-1">{judgeScore.submittedAt}</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 = () => (
<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>
<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>
</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>
</>
)
}