建立檔案
This commit is contained in:
661
components/competition/award-detail-dialog.tsx
Normal file
661
components/competition/award-detail-dialog.tsx
Normal file
@@ -0,0 +1,661 @@
|
||||
"use client"
|
||||
|
||||
import { useState } 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,
|
||||
} from "lucide-react"
|
||||
import type { Award as AwardType } from "@/types/competition"
|
||||
|
||||
interface AwardDetailDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
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 competition = competitions.find((c) => c.id === award.competitionId)
|
||||
const judgeScores = getJudgeScores(award.id)
|
||||
const appData = getAppData(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-red-100 text-red-800 border-red-200"
|
||||
}`}
|
||||
>
|
||||
{award.awardName}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-6 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600 mb-1">
|
||||
{award.awardType === "popular" && award.competitionType === "team"
|
||||
? `${award.score}`
|
||||
: award.awardType === "popular"
|
||||
? `${award.score}`
|
||||
: award.score}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{award.competitionType === "proposal"
|
||||
? "評審評分"
|
||||
: award.awardType === "popular"
|
||||
? award.competitionType === "team"
|
||||
? "人氣指數"
|
||||
: "收藏數"
|
||||
: "評審評分"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600 mb-1">
|
||||
{award.year}年{award.month}月
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">獲獎時間</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600 mb-1">{award.creator}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{award.competitionType === "team"
|
||||
? "團隊"
|
||||
: award.competitionType === "proposal"
|
||||
? "提案團隊"
|
||||
: "創作者"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{competition && (
|
||||
<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>
|
||||
{competition.name}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
<strong>競賽描述:</strong>
|
||||
{competition.description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>競賽期間:</strong>
|
||||
{competition.startDate} ~ {competition.endDate}
|
||||
</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">
|
||||
{appData.appUrl && (
|
||||
<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">生產環境</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-green-300 text-green-700 hover:bg-green-100 bg-transparent"
|
||||
onClick={() => window.open(appData.appUrl, "_blank")}
|
||||
>
|
||||
訪問
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appData.demoUrl && (
|
||||
<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(appData.demoUrl, "_blank")}
|
||||
>
|
||||
體驗
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appData.githubUrl && (
|
||||
<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(appData.githubUrl, "_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">
|
||||
{appData.reports.map((report) => (
|
||||
<div
|
||||
key={report.id}
|
||||
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(report.type)}
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900">{report.name}</h4>
|
||||
<p className="text-sm text-gray-600">{report.description}</p>
|
||||
<div className="flex items-center space-x-4 mt-1 text-xs text-gray-500">
|
||||
<span>大小:{report.size}</span>
|
||||
<span>上傳:{report.uploadDate}</span>
|
||||
<span className="uppercase">{report.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handlePreview(report)}
|
||||
className="text-blue-600 border-blue-300 hover:bg-blue-50"
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
預覽
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderCompetitionPhotos = () => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Camera className="w-5 h-5 text-purple-500" />
|
||||
<span>競賽照片</span>
|
||||
</CardTitle>
|
||||
<CardDescription>競賽當天的精彩瞬間</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{competitionPhotos.map((photo, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative group cursor-pointer overflow-hidden rounded-lg border hover:shadow-lg transition-all"
|
||||
onClick={() => {
|
||||
setCurrentPhotoIndex(index)
|
||||
setShowPhotoGallery(true)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={photo.url || "/placeholder.svg"}
|
||||
alt={photo.title}
|
||||
className="w-full h-32 object-cover group-hover:scale-105 transition-transform"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all flex items-center justify-center">
|
||||
<ImageIcon className="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-2">
|
||||
<p className="text-white text-xs font-medium">{photo.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setCurrentPhotoIndex(0)
|
||||
setShowPhotoGallery(true)
|
||||
}}
|
||||
>
|
||||
<Camera className="w-4 h-4 mr-2" />
|
||||
查看所有照片
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
const renderJudgePanel = () => {
|
||||
if (!competition) return null
|
||||
|
||||
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 gap-4">
|
||||
{competition.judges.map((judgeId) => {
|
||||
const judge = judges.find((j) => j.id === judgeId)
|
||||
if (!judge) return null
|
||||
|
||||
return (
|
||||
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
<Avatar>
|
||||
<AvatarImage src={judge.avatar || "/placeholder.svg?height=40&width=40"} />
|
||||
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium">{judge.name}</h4>
|
||||
<p className="text-sm text-gray-600">{judge.title}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{judge.expertise.slice(0, 2).map((skill) => (
|
||||
<Badge key={skill} variant="secondary" className="text-xs">
|
||||
{skill}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const renderJudgeScores = () => (
|
||||
<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>
|
||||
<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">
|
||||
{(judgeScores.reduce((sum, score) => sum + score.overallScore, 0) / judgeScores.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(...judgeScores.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(...judgeScores.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">{judgeScores.length}</div>
|
||||
<div className="text-sm text-purple-600">評審人數</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Individual Judge Scores */}
|
||||
{judgeScores.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 || "/placeholder.svg"} />
|
||||
<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>
|
||||
)
|
||||
|
||||
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>
|
||||
{competition?.name} - {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 */}
|
||||
<Dialog open={showPhotoGallery} onOpenChange={setShowPhotoGallery}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] p-0">
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2 z-10 bg-black bg-opacity-50 text-white hover:bg-opacity-70"
|
||||
onClick={() => setShowPhotoGallery(false)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<div className="relative">
|
||||
<img
|
||||
src={competitionPhotos[currentPhotoIndex]?.url || "/placeholder.svg"}
|
||||
alt={competitionPhotos[currentPhotoIndex]?.title}
|
||||
className="w-full h-96 object-cover"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute left-2 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 text-white hover:bg-opacity-70"
|
||||
onClick={prevPhoto}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 text-white hover:bg-opacity-70"
|
||||
onClick={nextPhoto}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-4">
|
||||
<h3 className="text-white text-lg font-semibold">{competitionPhotos[currentPhotoIndex]?.title}</h3>
|
||||
<p className="text-white text-sm opacity-90">{competitionPhotos[currentPhotoIndex]?.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
{competitionPhotos.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${
|
||||
index === currentPhotoIndex ? "bg-purple-600" : "bg-gray-300"
|
||||
}`}
|
||||
onClick={() => setCurrentPhotoIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center text-sm text-gray-500 mt-2">
|
||||
{currentPhotoIndex + 1} / {competitionPhotos.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
731
components/competition/competition-detail-dialog.tsx
Normal file
731
components/competition/competition-detail-dialog.tsx
Normal file
@@ -0,0 +1,731 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
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 { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Target,
|
||||
Users,
|
||||
Lightbulb,
|
||||
Star,
|
||||
Heart,
|
||||
Eye,
|
||||
Trophy,
|
||||
Crown,
|
||||
UserCheck,
|
||||
Building,
|
||||
Mail,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
MessageSquare,
|
||||
ImageIcon,
|
||||
Mic,
|
||||
TrendingUp,
|
||||
Brain,
|
||||
Zap,
|
||||
Play,
|
||||
} from "lucide-react"
|
||||
|
||||
// AI applications data - empty for production
|
||||
const aiApps: any[] = []
|
||||
|
||||
interface CompetitionDetailDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
ranking: any
|
||||
competitionType: "individual" | "team" | "proposal"
|
||||
}
|
||||
|
||||
export function CompetitionDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
ranking,
|
||||
competitionType,
|
||||
}: CompetitionDetailDialogProps) {
|
||||
const { user, getAppLikes, getViewCount, getAppRating, incrementViewCount, addToRecentApps } = useAuth()
|
||||
const { judges, getAppJudgeScores, getProposalJudgeScores, getTeamById, getProposalById, currentCompetition } =
|
||||
useCompetition()
|
||||
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
|
||||
const handleTryApp = (appId: string) => {
|
||||
incrementViewCount(appId)
|
||||
addToRecentApps(appId)
|
||||
console.log(`Opening app ${appId}`)
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
文字處理: "bg-blue-100 text-blue-800 border-blue-200",
|
||||
圖像生成: "bg-purple-100 text-purple-800 border-purple-200",
|
||||
語音辨識: "bg-green-100 text-green-800 border-green-200",
|
||||
推薦系統: "bg-orange-100 text-orange-800 border-orange-200",
|
||||
}
|
||||
return colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800 border-gray-200"
|
||||
}
|
||||
|
||||
const renderIndividualDetail = () => {
|
||||
const app = aiApps.find((a) => a.id === ranking.appId)
|
||||
const judgeScores = getAppJudgeScores(ranking.appId!)
|
||||
const likes = getAppLikes(ranking.appId!)
|
||||
const views = getViewCount(ranking.appId!)
|
||||
const rating = getAppRating(ranking.appId!)
|
||||
|
||||
if (!app) return null
|
||||
|
||||
const IconComponent = app.icon
|
||||
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">應用概覽</TabsTrigger>
|
||||
<TabsTrigger value="scores">評審評分</TabsTrigger>
|
||||
<TabsTrigger value="experience">立即體驗</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-blue-500 to-purple-500 rounded-xl flex items-center justify-center">
|
||||
<IconComponent className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-2xl">{app.name}</CardTitle>
|
||||
<CardDescription className="text-lg">by {app.creator}</CardDescription>
|
||||
<div className="flex items-center space-x-4 mt-2">
|
||||
<Badge variant="outline" className={getTypeColor(app.type)}>
|
||||
{app.type}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-gray-100 text-gray-700 border-gray-200">
|
||||
{app.department}
|
||||
</Badge>
|
||||
<div className="flex items-center space-x-1 text-2xl font-bold text-purple-600">
|
||||
<Trophy className="w-6 h-6" />
|
||||
<span>第 {ranking.rank} 名</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-700 mb-6">{app.description}</p>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-center space-x-1 text-2xl font-bold text-purple-600">
|
||||
<Star className="w-6 h-6" />
|
||||
<span>{ranking.totalScore.toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">總評分</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-center space-x-1 text-2xl font-bold text-red-600">
|
||||
<Heart className="w-6 h-6" />
|
||||
<span>{likes}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">收藏數</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-center space-x-1 text-2xl font-bold text-blue-600">
|
||||
<Eye className="w-6 h-6" />
|
||||
<span>{views}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">瀏覽數</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-center space-x-1 text-2xl font-bold text-yellow-600">
|
||||
<Star className="w-6 h-6" />
|
||||
<span>{rating.toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">用戶評分</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
{[
|
||||
{ key: "innovation", name: "創新性", icon: "💡", color: "text-yellow-600" },
|
||||
{ key: "technical", name: "技術性", icon: "⚙️", color: "text-blue-600" },
|
||||
{ key: "usability", name: "實用性", icon: "🎯", color: "text-green-600" },
|
||||
{ key: "presentation", name: "展示效果", icon: "🎨", color: "text-purple-600" },
|
||||
{ key: "impact", name: "影響力", icon: "🚀", color: "text-red-600" },
|
||||
].map((category) => (
|
||||
<div key={category.key} className="text-center p-4 bg-white border rounded-lg">
|
||||
<div className="text-2xl mb-2">{category.icon}</div>
|
||||
<div className={`text-xl font-bold ${category.color}`}>
|
||||
{ranking.scores[category.key as keyof typeof ranking.scores].toFixed(1)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">{category.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="scores" className="space-y-6">
|
||||
<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="space-y-6">
|
||||
{judgeScores.map((score) => {
|
||||
const judge = judges.find((j) => j.id === score.judgeId)
|
||||
if (!judge) return null
|
||||
|
||||
const totalScore = Object.values(score.scores).reduce((sum, s) => sum + s, 0) / 5
|
||||
|
||||
return (
|
||||
<div key={score.judgeId} className="border rounded-lg p-6">
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
||||
<AvatarFallback className="bg-purple-100 text-purple-700 text-lg">
|
||||
{judge.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-lg">{judge.name}</h4>
|
||||
<p className="text-gray-600">{judge.title}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{judge.expertise.map((skill) => (
|
||||
<Badge key={skill} variant="secondary" className="text-xs">
|
||||
{skill}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-purple-600">{totalScore.toFixed(1)}</div>
|
||||
<div className="text-sm text-gray-500">總分</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 gap-3 mb-4">
|
||||
{[
|
||||
{ key: "innovation", name: "創新性", icon: "💡" },
|
||||
{ key: "technical", name: "技術性", icon: "⚙️" },
|
||||
{ key: "usability", name: "實用性", icon: "🎯" },
|
||||
{ key: "presentation", name: "展示效果", icon: "🎨" },
|
||||
{ key: "impact", name: "影響力", icon: "🚀" },
|
||||
].map((category) => (
|
||||
<div key={category.key} className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-lg">{category.icon}</div>
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
{score.scores[category.key as keyof typeof score.scores]}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{category.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<h5 className="font-medium text-blue-900 mb-2">評審意見</h5>
|
||||
<p className="text-blue-800">{score.comments}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="experience" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Play className="w-5 h-5 text-green-500" />
|
||||
<span>立即體驗應用</span>
|
||||
</CardTitle>
|
||||
<CardDescription>體驗這個獲獎的 AI 應用</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center py-12">
|
||||
<div className="w-24 h-24 bg-gradient-to-r from-blue-500 to-purple-500 rounded-xl flex items-center justify-center mx-auto mb-6">
|
||||
<IconComponent className="w-12 h-12 text-white" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold mb-4">{app.name}</h3>
|
||||
<p className="text-gray-600 mb-8 max-w-md mx-auto">{app.description}</p>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||||
onClick={() => handleTryApp(app.id)}
|
||||
>
|
||||
<Play className="w-5 h-5 mr-2" />
|
||||
立即體驗
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTeamDetail = () => {
|
||||
const team = getTeamById(ranking.teamId!)
|
||||
if (!team) return null
|
||||
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">團隊概覽</TabsTrigger>
|
||||
<TabsTrigger value="members">團隊成員</TabsTrigger>
|
||||
<TabsTrigger value="apps">團隊應用</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-green-500 to-blue-500 rounded-xl flex items-center justify-center">
|
||||
<Users className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-2xl">{team.name}</CardTitle>
|
||||
<CardDescription className="text-lg">
|
||||
隊長:{team.members.find((m) => m.id === team.leader)?.name}
|
||||
</CardDescription>
|
||||
<div className="flex items-center space-x-4 mt-2">
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800 border-green-200">
|
||||
團隊賽
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-gray-100 text-gray-700 border-gray-200">
|
||||
{team.department}
|
||||
</Badge>
|
||||
<div className="flex items-center space-x-1 text-2xl font-bold text-green-600">
|
||||
<Trophy className="w-6 h-6" />
|
||||
<span>第 {ranking.rank} 名</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-center space-x-1 text-2xl font-bold text-green-600">
|
||||
<Star className="w-6 h-6" />
|
||||
<span>{ranking.totalScore.toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">團隊評分</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">{team.apps.length}</div>
|
||||
<div className="text-sm text-gray-500 mt-1">提交應用</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">{team.totalLikes}</div>
|
||||
<div className="text-sm text-gray-500 mt-1">總按讚數</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600">{team.members.length}</div>
|
||||
<div className="text-sm text-gray-500 mt-1">團隊成員</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Building className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm">{team.department}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Mail className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm">{team.contactEmail}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-green-600 font-medium">
|
||||
人氣指數: {Math.round((team.totalLikes / team.apps.length) * 10) / 10}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="members" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<UserCheck className="w-5 h-5 text-green-500" />
|
||||
<span>團隊成員</span>
|
||||
</CardTitle>
|
||||
<CardDescription>團隊所有成員的詳細資訊</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{team.members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={`border rounded-lg p-4 ${
|
||||
member.id === team.leader ? "border-green-300 bg-green-50" : "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar>
|
||||
<AvatarImage src={`/placeholder-40x40.png?height=40&width=40&text=${member.name[0]}`} />
|
||||
<AvatarFallback className="bg-green-100 text-green-700">{member.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="font-semibold">{member.name}</h4>
|
||||
{member.id === team.leader && (
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||
隊長
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{member.role}</p>
|
||||
<Badge variant="outline" className="text-xs mt-1">
|
||||
{member.department}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="apps" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Trophy className="w-5 h-5 text-blue-500" />
|
||||
<span>團隊應用</span>
|
||||
</CardTitle>
|
||||
<CardDescription>團隊提交的所有應用</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{team.apps.map((appId) => {
|
||||
const app = aiApps.find((a) => a.id === appId)
|
||||
if (!app) return null
|
||||
|
||||
const IconComponent = app.icon
|
||||
const likes = getAppLikes(appId)
|
||||
const views = getViewCount(appId)
|
||||
const rating = getAppRating(appId)
|
||||
|
||||
return (
|
||||
<Card key={appId} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-blue-100 to-purple-100 rounded-lg flex items-center justify-center">
|
||||
<IconComponent className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{app.name}</CardTitle>
|
||||
<CardDescription>by {app.creator}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-600 mb-4">{app.description}</p>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Heart className="w-3 h-3" />
|
||||
<span>{likes}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="w-3 h-3" />
|
||||
<span>{views}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="w-3 h-3 text-yellow-500" />
|
||||
<span>{rating.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full bg-transparent"
|
||||
variant="outline"
|
||||
onClick={() => handleTryApp(app.id)}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
體驗應用
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
const renderProposalDetail = () => {
|
||||
const proposal = getProposalById(ranking.proposalId!)
|
||||
const team = ranking.team
|
||||
const judgeScores = getProposalJudgeScores(ranking.proposalId!)
|
||||
|
||||
if (!proposal) return null
|
||||
|
||||
return (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">提案概覽</TabsTrigger>
|
||||
<TabsTrigger value="scores">評審評分</TabsTrigger>
|
||||
<TabsTrigger value="team">提案團隊</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl flex items-center justify-center">
|
||||
<Lightbulb className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-2xl">{proposal.title}</CardTitle>
|
||||
<CardDescription className="text-lg">提案團隊:{team?.name}</CardDescription>
|
||||
<div className="flex items-center space-x-4 mt-2">
|
||||
<Badge variant="outline" className="bg-purple-100 text-purple-800 border-purple-200">
|
||||
提案賽
|
||||
</Badge>
|
||||
<div className="flex items-center space-x-1 text-2xl font-bold text-purple-600">
|
||||
<Trophy className="w-6 h-6" />
|
||||
<span>第 {ranking.rank} 名</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-3xl font-bold text-purple-600 mb-2">{ranking.totalScore.toFixed(1)}</div>
|
||||
<div className="text-gray-500">總評分</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<h5 className="font-semibold text-red-900 mb-2 flex items-center">
|
||||
<AlertCircle className="w-5 h-5 mr-2" />
|
||||
痛點描述
|
||||
</h5>
|
||||
<p className="text-red-800">{proposal.problemStatement}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<h5 className="font-semibold text-green-900 mb-2 flex items-center">
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
解決方案
|
||||
</h5>
|
||||
<p className="text-green-800">{proposal.solution}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h5 className="font-semibold text-blue-900 mb-2 flex items-center">
|
||||
<Target className="w-5 h-5 mr-2" />
|
||||
預期影響
|
||||
</h5>
|
||||
<p className="text-blue-800">{proposal.expectedImpact}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-6" />
|
||||
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
{[
|
||||
{ key: "problemIdentification", name: "問題識別", icon: "🔍", color: "text-red-600" },
|
||||
{ key: "solutionFeasibility", name: "方案可行性", icon: "⚙️", color: "text-blue-600" },
|
||||
{ key: "innovation", name: "創新性", icon: "💡", color: "text-yellow-600" },
|
||||
{ key: "impact", name: "預期影響", icon: "🚀", color: "text-green-600" },
|
||||
{ key: "presentation", name: "展示效果", icon: "🎨", color: "text-purple-600" },
|
||||
].map((category) => (
|
||||
<div key={category.key} className="text-center p-4 bg-white border rounded-lg">
|
||||
<div className="text-2xl mb-2">{category.icon}</div>
|
||||
<div className={`text-xl font-bold ${category.color}`}>
|
||||
{ranking.scores[category.key as keyof typeof ranking.scores].toFixed(1)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">{category.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="scores" className="space-y-6">
|
||||
<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="space-y-6">
|
||||
{judgeScores.map((score) => {
|
||||
const judge = judges.find((j) => j.id === score.judgeId)
|
||||
if (!judge) return null
|
||||
|
||||
const totalScore = Object.values(score.scores).reduce((sum, s) => sum + s, 0) / 5
|
||||
|
||||
return (
|
||||
<div key={score.judgeId} className="border rounded-lg p-6">
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
||||
<AvatarFallback className="bg-purple-100 text-purple-700 text-lg">
|
||||
{judge.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-lg">{judge.name}</h4>
|
||||
<p className="text-gray-600">{judge.title}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{judge.expertise.map((skill) => (
|
||||
<Badge key={skill} variant="secondary" className="text-xs">
|
||||
{skill}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-purple-600">{totalScore.toFixed(1)}</div>
|
||||
<div className="text-sm text-gray-500">總分</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 gap-3 mb-4">
|
||||
{[
|
||||
{ key: "problemIdentification", name: "問題識別", icon: "🔍" },
|
||||
{ key: "solutionFeasibility", name: "方案可行性", icon: "⚙️" },
|
||||
{ key: "innovation", name: "創新性", icon: "💡" },
|
||||
{ key: "impact", name: "預期影響", icon: "🚀" },
|
||||
{ key: "presentation", name: "展示效果", icon: "🎨" },
|
||||
].map((category) => (
|
||||
<div key={category.key} className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-lg">{category.icon}</div>
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
{score.scores[category.key as keyof typeof score.scores]}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{category.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<h5 className="font-medium text-purple-900 mb-2">評審意見</h5>
|
||||
<p className="text-purple-800">{score.comments}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="team" className="space-y-6">
|
||||
{team && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
<span>提案團隊</span>
|
||||
</CardTitle>
|
||||
<CardDescription>提案團隊的詳細資訊</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-6">
|
||||
<h4 className="font-semibold text-lg mb-2">{team.name}</h4>
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-600">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Building className="w-4 h-4" />
|
||||
<span>{team.department}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Mail className="w-4 h-4" />
|
||||
<span>{team.contactEmail}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>提交於 {new Date(proposal.submittedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{team.members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={`border rounded-lg p-4 ${
|
||||
member.id === team.leader ? "border-green-300 bg-green-50" : "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar>
|
||||
<AvatarImage src={`/placeholder-40x40.png?height=40&width=40&text=${member.name[0]}`} />
|
||||
<AvatarFallback className="bg-green-100 text-green-700">{member.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="font-semibold">{member.name}</h4>
|
||||
{member.id === team.leader && (
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||
隊長
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{member.role}</p>
|
||||
<Badge variant="outline" className="text-xs mt-1">
|
||||
{member.department}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">
|
||||
{competitionType === "individual" && "個人賽詳情"}
|
||||
{competitionType === "team" && "團隊賽詳情"}
|
||||
{competitionType === "proposal" && "提案賽詳情"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{currentCompetition?.name} - 第 {ranking.rank} 名
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-6">
|
||||
{competitionType === "individual" && renderIndividualDetail()}
|
||||
{competitionType === "team" && renderTeamDetail()}
|
||||
{competitionType === "proposal" && renderProposalDetail()}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
261
components/competition/judge-scoring-dialog.tsx
Normal file
261
components/competition/judge-scoring-dialog.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useCompetition } from "@/contexts/competition-context"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Star, User, Award, MessageSquare, CheckCircle } from "lucide-react"
|
||||
|
||||
interface JudgeScoringDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
appId: string
|
||||
appName: string
|
||||
judgeId: string
|
||||
}
|
||||
|
||||
export function JudgeScoringDialog({ open, onOpenChange, appId, appName, judgeId }: JudgeScoringDialogProps) {
|
||||
const { judges, submitJudgeScore, getAppJudgeScores } = useCompetition()
|
||||
|
||||
const judge = judges.find((j) => j.id === judgeId)
|
||||
const existingScore = getAppJudgeScores(appId).find((s) => s.judgeId === judgeId)
|
||||
|
||||
const [scores, setScores] = useState({
|
||||
innovation: existingScore?.scores.innovation || 8,
|
||||
technical: existingScore?.scores.technical || 8,
|
||||
usability: existingScore?.scores.usability || 8,
|
||||
presentation: existingScore?.scores.presentation || 8,
|
||||
impact: existingScore?.scores.impact || 8,
|
||||
})
|
||||
|
||||
const [comments, setComments] = useState(existingScore?.comments || "")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
|
||||
if (!judge) return null
|
||||
|
||||
const handleScoreChange = (category: keyof typeof scores, value: number[]) => {
|
||||
setScores((prev) => ({
|
||||
...prev,
|
||||
[category]: value[0],
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!comments.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
submitJudgeScore({
|
||||
judgeId,
|
||||
appId,
|
||||
scores,
|
||||
comments: comments.trim(),
|
||||
})
|
||||
|
||||
setIsSubmitting(false)
|
||||
setIsSubmitted(true)
|
||||
|
||||
// Auto close after success
|
||||
setTimeout(() => {
|
||||
setIsSubmitted(false)
|
||||
onOpenChange(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const totalScore = Object.values(scores).reduce((sum, score) => sum + score, 0)
|
||||
const averageScore = (totalScore / 5).toFixed(1)
|
||||
|
||||
const scoreCategories = [
|
||||
{
|
||||
key: "innovation" as const,
|
||||
name: "創新性",
|
||||
description: "技術創新程度和獨特性",
|
||||
icon: "💡",
|
||||
},
|
||||
{
|
||||
key: "technical" as const,
|
||||
name: "技術性",
|
||||
description: "技術實現難度和完成度",
|
||||
icon: "⚙️",
|
||||
},
|
||||
{
|
||||
key: "usability" as const,
|
||||
name: "實用性",
|
||||
description: "實際應用價值和用戶體驗",
|
||||
icon: "🎯",
|
||||
},
|
||||
{
|
||||
key: "presentation" as const,
|
||||
name: "展示效果",
|
||||
description: "介面設計和展示完整性",
|
||||
icon: "🎨",
|
||||
},
|
||||
{
|
||||
key: "impact" as const,
|
||||
name: "影響力",
|
||||
description: "對業務和用戶的潛在影響",
|
||||
icon: "🚀",
|
||||
},
|
||||
]
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<div className="flex flex-col items-center space-y-4 py-8">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">評分提交成功!</h3>
|
||||
<p className="text-gray-600">您對「{appName}」的評分已成功提交</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center space-x-2">
|
||||
<Award className="w-5 h-5 text-purple-600" />
|
||||
<span>評審評分</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>為「{appName}」進行專業評分</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Judge Info */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar>
|
||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
||||
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h4 className="font-semibold">{judge.name}</h4>
|
||||
<p className="text-sm text-gray-600">{judge.title}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{judge.expertise.map((skill) => (
|
||||
<Badge key={skill} variant="secondary" className="text-xs">
|
||||
{skill}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* Scoring Categories */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>評分項目</span>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-purple-600">{averageScore}</div>
|
||||
<div className="text-sm text-gray-500">平均分數</div>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardDescription>請根據各項目標準進行評分(1-10分)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{scoreCategories.map((category) => (
|
||||
<div key={category.key} className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-lg">{category.icon}</span>
|
||||
<div>
|
||||
<h4 className="font-medium">{category.name}</h4>
|
||||
<p className="text-sm text-gray-600">{category.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-purple-600">{scores[category.key]}</div>
|
||||
<div className="flex">
|
||||
{[...Array(scores[category.key])].map((_, i) => (
|
||||
<Star key={i} className="w-3 h-3 text-yellow-400 fill-current" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
value={[scores[category.key]]}
|
||||
onValueChange={(value) => handleScoreChange(category.key, value)}
|
||||
max={10}
|
||||
min={1}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>1分 (差)</span>
|
||||
<span>5分 (普通)</span>
|
||||
<span>10分 (優秀)</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Comments */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>評審意見</span>
|
||||
</CardTitle>
|
||||
<CardDescription>請提供詳細的評審意見和建議</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
placeholder="請分享您對此應用的專業評價、優點、改進建議等..."
|
||||
value={comments}
|
||||
onChange={(e) => setComments(e.target.value)}
|
||||
rows={4}
|
||||
maxLength={1000}
|
||||
/>
|
||||
<div className="text-xs text-gray-500 mt-2">{comments.length}/1000 字元</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="flex space-x-3">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || !comments.trim()}
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||||
>
|
||||
{isSubmitting ? "提交中..." : existingScore ? "更新評分" : "提交評分"}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{existingScore && (
|
||||
<Alert>
|
||||
<User className="h-4 w-4" />
|
||||
<AlertDescription>您已經為此應用評分過,提交將更新您的評分。</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
779
components/competition/popularity-rankings.tsx
Normal file
779
components/competition/popularity-rankings.tsx
Normal file
@@ -0,0 +1,779 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { useCompetition } from "@/contexts/competition-context"
|
||||
import {
|
||||
Search,
|
||||
Heart,
|
||||
Eye,
|
||||
Trophy,
|
||||
Calendar,
|
||||
Users,
|
||||
Target,
|
||||
Lightbulb,
|
||||
MessageSquare,
|
||||
ImageIcon,
|
||||
Mic,
|
||||
TrendingUp,
|
||||
Brain,
|
||||
Zap,
|
||||
Crown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ThumbsUp,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { LikeButton } from "@/components/like-button"
|
||||
import { AppDetailDialog } from "@/components/app-detail-dialog"
|
||||
import { TeamDetailDialog } from "@/components/competition/team-detail-dialog"
|
||||
|
||||
// AI applications data - empty for production
|
||||
const aiApps: any[] = []
|
||||
|
||||
// Teams data - empty for production
|
||||
const mockTeams: any[] = []
|
||||
|
||||
|
||||
export function PopularityRankings() {
|
||||
const { user, getLikeCount, getViewCount } = useAuth()
|
||||
const { competitions, currentCompetition, setCurrentCompetition, judges } = useCompetition()
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [selectedDepartment, setSelectedDepartment] = useState("all")
|
||||
const [selectedType, setSelectedType] = useState("all")
|
||||
const [selectedCompetitionType, setSelectedCompetitionType] = useState("all")
|
||||
const [selectedApp, setSelectedApp] = useState<any>(null)
|
||||
const [showAppDetail, setShowAppDetail] = useState(false)
|
||||
const [selectedTeam, setSelectedTeam] = useState<any>(null)
|
||||
const [showTeamDetail, setShowTeamDetail] = useState(false)
|
||||
const [individualCurrentPage, setIndividualCurrentPage] = useState(0)
|
||||
const [teamCurrentPage, setTeamCurrentPage] = useState(0)
|
||||
|
||||
const ITEMS_PER_PAGE = 3
|
||||
|
||||
// Filter apps based on search criteria
|
||||
const filteredApps = aiApps.filter((app) => {
|
||||
const matchesSearch =
|
||||
app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.creator.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesDepartment = selectedDepartment === "all" || app.department === selectedDepartment
|
||||
const matchesType = selectedType === "all" || app.type === selectedType
|
||||
const matchesCompetitionType = selectedCompetitionType === "all" || app.competitionType === selectedCompetitionType
|
||||
|
||||
return matchesSearch && matchesDepartment && matchesType && matchesCompetitionType
|
||||
})
|
||||
|
||||
// Sort apps by like count (popularity) and group by competition type
|
||||
const sortedApps = filteredApps.sort((a, b) => {
|
||||
const likesA = getLikeCount(a.id.toString())
|
||||
const likesB = getLikeCount(b.id.toString())
|
||||
return likesB - likesA
|
||||
})
|
||||
|
||||
// Group apps by competition type
|
||||
const individualApps = sortedApps.filter((app) => app.competitionType === "individual")
|
||||
const teamApps = sortedApps.filter((app) => app.competitionType === "team")
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
文字處理: "bg-blue-100 text-blue-800 border-blue-200",
|
||||
圖像生成: "bg-purple-100 text-purple-800 border-purple-200",
|
||||
語音辨識: "bg-green-100 text-green-800 border-green-200",
|
||||
推薦系統: "bg-orange-100 text-orange-800 border-orange-200",
|
||||
}
|
||||
return colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800 border-gray-200"
|
||||
}
|
||||
|
||||
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" />
|
||||
case "mixed":
|
||||
return <Trophy 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 "提案賽"
|
||||
case "mixed":
|
||||
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"
|
||||
case "mixed":
|
||||
return "bg-gradient-to-r from-blue-100 via-green-100 to-purple-100 text-gray-800 border-gray-200"
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800 border-gray-200"
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenAppDetail = (app: any) => {
|
||||
setSelectedApp(app)
|
||||
setShowAppDetail(true)
|
||||
}
|
||||
|
||||
const handleOpenTeamDetail = (team: any) => {
|
||||
setSelectedTeam(team)
|
||||
setShowTeamDetail(true)
|
||||
}
|
||||
|
||||
const renderCompetitionSection = (apps: any[], title: string, competitionType: string) => {
|
||||
if (apps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentPage = competitionType === "individual" ? individualCurrentPage : teamCurrentPage
|
||||
const setCurrentPage = competitionType === "individual" ? setIndividualCurrentPage : setTeamCurrentPage
|
||||
const totalPages = Math.ceil(apps.length / ITEMS_PER_PAGE)
|
||||
const startIndex = currentPage * ITEMS_PER_PAGE
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE
|
||||
const currentApps = apps.slice(startIndex, endIndex)
|
||||
|
||||
return (
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
{competitionType === "individual" ? (
|
||||
<Target className="w-5 h-5 text-blue-500" />
|
||||
) : (
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
)}
|
||||
<span>{title}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{currentCompetition?.name || "暫無進行中的競賽"} - {title}人氣排名 (共 {apps.length} 個應用)
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge variant="outline" className={getCompetitionTypeColor(competitionType)}>
|
||||
{getCompetitionTypeIcon(competitionType)}
|
||||
<span className="ml-1">{getCompetitionTypeText(competitionType)}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{/* Page indicator */}
|
||||
{totalPages > 1 && (
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-gray-600">
|
||||
顯示第 {startIndex + 1}-{Math.min(endIndex, apps.length)} 名,共 {apps.length} 個應用
|
||||
</div>
|
||||
<div className="flex justify-center items-center space-x-2 mt-2">
|
||||
{Array.from({ length: totalPages }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-200 ${
|
||||
index === currentPage ? "bg-blue-500 w-6" : "bg-gray-300"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Carousel Container */}
|
||||
<div className="relative">
|
||||
{/* Left Arrow */}
|
||||
{totalPages > 1 && currentPage > 0 && (
|
||||
<button
|
||||
onClick={() => setCurrentPage(Math.max(0, currentPage - 1))}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-6 z-10 w-12 h-12 bg-white rounded-full shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 hover:shadow-xl transition-all duration-200 group"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-600 group-hover:text-gray-800" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Right Arrow */}
|
||||
{totalPages > 1 && currentPage < totalPages - 1 && (
|
||||
<button
|
||||
onClick={() => setCurrentPage(Math.min(totalPages - 1, currentPage + 1))}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-6 z-10 w-12 h-12 bg-white rounded-full shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 hover:shadow-xl transition-all duration-200 group"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-gray-600 group-hover:text-gray-800" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Apps Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-8">
|
||||
{currentApps.map((app, index) => {
|
||||
const likes = getLikeCount(app.id.toString())
|
||||
const views = getViewCount(app.id.toString())
|
||||
const globalRank = startIndex + index + 1
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={app.id}
|
||||
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-yellow-50 to-orange-50 border border-yellow-200 flex flex-col"
|
||||
>
|
||||
<CardContent className="p-4 flex flex-col flex-1">
|
||||
<div className="flex items-start space-x-3 mb-4">
|
||||
{/* Numbered Badge */}
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-full flex items-center justify-center text-white font-bold text-xl shadow-md flex-shrink-0">
|
||||
{globalRank}
|
||||
</div>
|
||||
|
||||
{/* App Icon */}
|
||||
<div className="w-10 h-10 bg-white rounded-lg flex items-center justify-center border border-gray-200 shadow-sm flex-shrink-0">
|
||||
<MessageSquare className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold text-gray-900 mb-1 truncate">{app.name}</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">by {app.creator}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className={`${getTypeColor(app.type)} text-xs`}>
|
||||
{app.type}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-gray-100 text-gray-700 border-gray-200 text-xs">
|
||||
{app.department}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description - flexible height */}
|
||||
<div className="mb-4 flex-1">
|
||||
<p className="text-sm text-gray-600 line-clamp-3">{app.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="w-4 h-4" />
|
||||
<span>{views} 次瀏覽</span>
|
||||
</div>
|
||||
{globalRank <= 3 && (
|
||||
<div className="flex items-center space-x-1 text-orange-600 font-medium">
|
||||
<Trophy className="w-4 h-4" />
|
||||
<span>人氣冠軍</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - Always at bottom with consistent positioning */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-yellow-200 mt-auto">
|
||||
{/* Enhanced Like Button */}
|
||||
<div className="flex items-center">
|
||||
<LikeButton appId={app.id.toString()} size="lg" />
|
||||
</div>
|
||||
|
||||
{/* View Details Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white hover:bg-gray-50 border-gray-300 shadow-sm"
|
||||
onClick={() => handleOpenAppDetail(app)}
|
||||
>
|
||||
查看詳情
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTeamCompetitionSection = (teams: any[], title: string) => {
|
||||
if (teams.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate team popularity score: total apps × highest like count
|
||||
const teamsWithScores = teams
|
||||
.map((team) => {
|
||||
const appLikes = team.apps.map((appId: string) => getLikeCount(appId))
|
||||
const maxLikes = Math.max(...appLikes, 0)
|
||||
const totalApps = team.apps.length
|
||||
const popularityScore = totalApps * maxLikes
|
||||
|
||||
return {
|
||||
...team,
|
||||
popularityScore,
|
||||
maxLikes,
|
||||
totalApps,
|
||||
totalViews: team.apps.reduce((sum: number, appId: string) => sum + getViewCount(appId), 0),
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.popularityScore - a.popularityScore)
|
||||
|
||||
const currentPage = teamCurrentPage
|
||||
const setCurrentPage = setTeamCurrentPage
|
||||
const totalPages = Math.ceil(teamsWithScores.length / ITEMS_PER_PAGE)
|
||||
const startIndex = currentPage * ITEMS_PER_PAGE
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE
|
||||
const currentTeams = teamsWithScores.slice(startIndex, endIndex)
|
||||
|
||||
return (
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
<span>{title}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{currentCompetition?.name || "暫無進行中的競賽"} - {title}人氣排名 (共 {teamsWithScores.length} 個團隊)
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800 border-green-200">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="ml-1">團隊賽</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{/* Page indicator */}
|
||||
{totalPages > 1 && (
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-gray-600">
|
||||
顯示第 {startIndex + 1}-{Math.min(endIndex, teamsWithScores.length)} 名,共 {teamsWithScores.length}{" "}
|
||||
個團隊
|
||||
</div>
|
||||
<div className="flex justify-center items-center space-x-2 mt-2">
|
||||
{Array.from({ length: totalPages }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-200 ${
|
||||
index === currentPage ? "bg-green-500 w-6" : "bg-gray-300"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Carousel Container */}
|
||||
<div className="relative">
|
||||
{/* Left Arrow */}
|
||||
{totalPages > 1 && currentPage > 0 && (
|
||||
<button
|
||||
onClick={() => setCurrentPage(Math.max(0, currentPage - 1))}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-6 z-10 w-12 h-12 bg-white rounded-full shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 hover:shadow-xl transition-all duration-200 group"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-600 group-hover:text-gray-800" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Right Arrow */}
|
||||
{totalPages > 1 && currentPage < totalPages - 1 && (
|
||||
<button
|
||||
onClick={() => setCurrentPage(Math.min(totalPages - 1, currentPage + 1))}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-6 z-10 w-12 h-12 bg-white rounded-full shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 hover:shadow-xl transition-all duration-200 group"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-gray-600 group-hover:text-gray-800" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Teams Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-8">
|
||||
{currentTeams.map((team, index) => {
|
||||
const globalRank = startIndex + index + 1
|
||||
const leader = team.members.find((m: any) => m.id === team.leader)
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={team.id}
|
||||
className="hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-green-50 to-blue-50 border border-green-200 flex flex-col"
|
||||
>
|
||||
<CardContent className="p-4 flex flex-col flex-1">
|
||||
<div className="flex items-start space-x-3 mb-4">
|
||||
{/* Numbered Badge */}
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-green-400 to-blue-400 rounded-full flex items-center justify-center text-white font-bold text-xl shadow-md flex-shrink-0">
|
||||
{globalRank}
|
||||
</div>
|
||||
|
||||
{/* Team Icon */}
|
||||
<div className="w-10 h-10 bg-white rounded-lg flex items-center justify-center border border-gray-200 shadow-sm flex-shrink-0">
|
||||
<Users className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold text-gray-900 mb-1 truncate">{team.name}</h4>
|
||||
<p className="text-sm text-gray-600 mb-2">隊長:{leader?.name}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="bg-green-100 text-green-800 border-green-200 text-xs">
|
||||
團隊賽
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-gray-100 text-gray-700 border-gray-200 text-xs">
|
||||
{team.department}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Members */}
|
||||
<div className="mb-4 flex-1">
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-2">團隊成員 ({team.members.length}人)</h5>
|
||||
<div className="space-y-1">
|
||||
{team.members.slice(0, 3).map((member: any) => (
|
||||
<div key={member.id} className="flex items-center space-x-2 text-xs">
|
||||
<div className="w-6 h-6 bg-green-100 rounded-full flex items-center justify-center text-green-700 font-medium">
|
||||
{member.name[0]}
|
||||
</div>
|
||||
<span className="text-gray-600">{member.name}</span>
|
||||
<span className="text-gray-400">({member.role})</span>
|
||||
</div>
|
||||
))}
|
||||
{team.members.length > 3 && (
|
||||
<div className="text-xs text-gray-500 ml-8">還有 {team.members.length - 3} 位成員...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Apps Info */}
|
||||
<div className="mb-4">
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-2">提交應用</h5>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="bg-white p-2 rounded border">
|
||||
<div className="font-bold text-blue-600">{team.totalApps}</div>
|
||||
<div className="text-gray-500">應用數量</div>
|
||||
</div>
|
||||
<div className="bg-white p-2 rounded border">
|
||||
<div className="font-bold text-red-600">{team.maxLikes}</div>
|
||||
<div className="text-gray-500">最高按讚</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="w-4 h-4" />
|
||||
<span>{team.totalViews} 次瀏覽</span>
|
||||
</div>
|
||||
{globalRank <= 3 && (
|
||||
<div className="flex items-center space-x-1 text-green-600 font-medium">
|
||||
<Trophy className="w-4 h-4" />
|
||||
<span>人氣前三</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Popularity Score */}
|
||||
<div className="mb-4 p-3 bg-gradient-to-r from-green-100 to-blue-100 rounded-lg">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-green-700">{team.popularityScore}</div>
|
||||
<div className="text-xs text-green-600">人氣指數</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{team.totalApps} 個應用 × {team.maxLikes} 最高按讚
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-green-200 mt-auto">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ThumbsUp className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">
|
||||
{team.apps.reduce((sum: number, appId: string) => sum + getLikeCount(appId), 0)} 總按讚
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white hover:bg-gray-50 border-gray-300 shadow-sm"
|
||||
onClick={() => handleOpenTeamDetail(team)}
|
||||
>
|
||||
查看團隊
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Competition Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-500" />
|
||||
<span>競賽人氣排行</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{currentCompetition?.name || "暫無進行中的競賽"} - 基於用戶按讚數的即時人氣排名
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Select
|
||||
value={currentCompetition?.id || ""}
|
||||
onValueChange={(value) => {
|
||||
const competition = competitions.find((c) => c.id === value)
|
||||
setCurrentCompetition(competition || null)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-64">
|
||||
<SelectValue placeholder="選擇競賽" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{competitions.map((competition) => (
|
||||
<SelectItem key={competition.id} value={competition.id}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getCompetitionTypeIcon(competition.type)}
|
||||
<span>{competition.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{currentCompetition && (
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm">
|
||||
{currentCompetition.year}年{currentCompetition.month}月
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className={getCompetitionTypeColor(currentCompetition.type)}>
|
||||
{getCompetitionTypeIcon(currentCompetition.type)}
|
||||
<span className="ml-1">{getCompetitionTypeText(currentCompetition.type)}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Users className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm">{filteredApps.length} 個參賽應用</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge
|
||||
variant={currentCompetition.status === "completed" ? "secondary" : "default"}
|
||||
className={
|
||||
currentCompetition.status === "completed"
|
||||
? "bg-green-100 text-green-800"
|
||||
: currentCompetition.status === "judging"
|
||||
? "bg-orange-100 text-orange-800"
|
||||
: currentCompetition.status === "active"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: "bg-purple-100 text-purple-800"
|
||||
}
|
||||
>
|
||||
{currentCompetition.status === "completed"
|
||||
? "已完成"
|
||||
: currentCompetition.status === "judging"
|
||||
? "評審中"
|
||||
: currentCompetition.status === "active"
|
||||
? "進行中"
|
||||
: "即將開始"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 mt-4">{currentCompetition.description}</p>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Judge Panel */}
|
||||
<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-4">
|
||||
{judges.map((judge) => (
|
||||
<div key={judge.id} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
<Avatar>
|
||||
<AvatarImage src={judge.avatar || "/placeholder.svg"} />
|
||||
<AvatarFallback className="bg-purple-100 text-purple-700">{judge.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium">{judge.name}</h4>
|
||||
<p className="text-sm text-gray-600">{judge.title}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{judge.expertise.slice(0, 2).map((skill) => (
|
||||
<Badge key={skill} variant="secondary" className="text-xs">
|
||||
{skill}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Search and Filter Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>篩選條件</CardTitle>
|
||||
<CardDescription>根據部門、應用類型、競賽類型或關鍵字篩選參賽應用</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col lg:flex-row gap-4 items-center">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="搜尋應用名稱、描述或創作者..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Select value={selectedCompetitionType} onValueChange={setSelectedCompetitionType}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="競賽類型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部類型</SelectItem>
|
||||
<SelectItem value="individual">個人賽</SelectItem>
|
||||
<SelectItem value="team">團隊賽</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={selectedDepartment} onValueChange={setSelectedDepartment}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="部門" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部部門</SelectItem>
|
||||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||||
<SelectItem value="SBU">SBU</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={selectedType} onValueChange={setSelectedType}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="應用類型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部類型</SelectItem>
|
||||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Competition Rankings */}
|
||||
{currentCompetition ? (
|
||||
<div className="space-y-8">
|
||||
{/* Individual Competition Section */}
|
||||
{(selectedCompetitionType === "all" || selectedCompetitionType === "individual") &&
|
||||
renderCompetitionSection(individualApps, "個人賽", "individual")}
|
||||
|
||||
{/* Team Competition Section */}
|
||||
{(selectedCompetitionType === "all" || selectedCompetitionType === "team") &&
|
||||
renderTeamCompetitionSection(
|
||||
mockTeams.filter(
|
||||
(team) =>
|
||||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
team.members.some((member: any) => member.name.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
selectedDepartment === "all" ||
|
||||
team.department === selectedDepartment,
|
||||
),
|
||||
"團隊賽",
|
||||
)}
|
||||
|
||||
{/* No Results */}
|
||||
{filteredApps.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="text-center py-12">
|
||||
<Heart className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-gray-600 mb-2">沒有找到符合條件的應用</h3>
|
||||
<p className="text-gray-500">請調整篩選條件或清除搜尋關鍵字</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4 bg-transparent"
|
||||
onClick={() => {
|
||||
setSearchTerm("")
|
||||
setSelectedDepartment("all")
|
||||
setSelectedType("all")
|
||||
setSelectedCompetitionType("all")
|
||||
}}
|
||||
>
|
||||
重置篩選條件
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="text-center py-12">
|
||||
<Trophy className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-gray-600 mb-2">請選擇競賽</h3>
|
||||
<p className="text-gray-500">選擇一個競賽來查看人氣排行榜</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* App Detail Dialog */}
|
||||
{selectedApp && <AppDetailDialog open={showAppDetail} onOpenChange={setShowAppDetail} app={selectedApp} />}
|
||||
|
||||
{/* Team Detail Dialog */}
|
||||
{selectedTeam && <TeamDetailDialog open={showTeamDetail} onOpenChange={setShowTeamDetail} team={selectedTeam} />}
|
||||
</div>
|
||||
)
|
||||
}
|
477
components/competition/registration-dialog.tsx
Normal file
477
components/competition/registration-dialog.tsx
Normal file
@@ -0,0 +1,477 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { useCompetition } from "@/contexts/competition-context"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { ChevronLeft, ChevronRight, Trophy, Users, FileText, CheckCircle, Target, Award, Loader2 } from "lucide-react"
|
||||
|
||||
interface RegistrationDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
interface RegistrationData {
|
||||
// 應用資訊
|
||||
appName: string
|
||||
appDescription: string
|
||||
appType: string
|
||||
techStack: string
|
||||
mainFeatures: string
|
||||
|
||||
// 團隊資訊
|
||||
teamName: string
|
||||
teamSize: string
|
||||
contactName: string
|
||||
contactEmail: string
|
||||
contactPhone: string
|
||||
department: string
|
||||
|
||||
// 參賽動機
|
||||
motivation: string
|
||||
expectedOutcome: string
|
||||
agreeTerms: boolean
|
||||
}
|
||||
|
||||
export function RegistrationDialog({ open, onOpenChange }: RegistrationDialogProps) {
|
||||
const { user } = useAuth()
|
||||
const { currentCompetition } = useCompetition()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const [applicationId, setApplicationId] = useState("")
|
||||
|
||||
const [formData, setFormData] = useState<RegistrationData>({
|
||||
appName: "",
|
||||
appDescription: "",
|
||||
appType: "",
|
||||
techStack: "",
|
||||
mainFeatures: "",
|
||||
teamName: "",
|
||||
teamSize: "1",
|
||||
contactName: user?.name || "",
|
||||
contactEmail: user?.email || "",
|
||||
contactPhone: "",
|
||||
department: "",
|
||||
motivation: "",
|
||||
expectedOutcome: "",
|
||||
agreeTerms: false,
|
||||
})
|
||||
|
||||
const totalSteps = 3
|
||||
const progress = (currentStep / totalSteps) * 100
|
||||
|
||||
const handleInputChange = (field: keyof RegistrationData, value: string | boolean) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const validateStep = (step: number): boolean => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return !!(formData.appName && formData.appDescription && formData.appType && formData.techStack)
|
||||
case 2:
|
||||
return !!(formData.teamName && formData.contactName && formData.contactEmail && formData.department)
|
||||
case 3:
|
||||
return !!(formData.motivation && formData.agreeTerms)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (validateStep(currentStep) && currentStep < totalSteps) {
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep((prev) => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateStep(3)) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
// 模擬提交過程
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// 生成申請編號
|
||||
const id = `REG${Date.now().toString().slice(-6)}`
|
||||
setApplicationId(id)
|
||||
setIsSubmitted(true)
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (isSubmitted) {
|
||||
// 重置表單
|
||||
setCurrentStep(1)
|
||||
setIsSubmitted(false)
|
||||
setApplicationId("")
|
||||
setFormData({
|
||||
appName: "",
|
||||
appDescription: "",
|
||||
appType: "",
|
||||
techStack: "",
|
||||
mainFeatures: "",
|
||||
teamName: "",
|
||||
teamSize: "1",
|
||||
contactName: user?.name || "",
|
||||
contactEmail: user?.email || "",
|
||||
contactPhone: "",
|
||||
department: "",
|
||||
motivation: "",
|
||||
expectedOutcome: "",
|
||||
agreeTerms: false,
|
||||
})
|
||||
}
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const renderStepContent = () => {
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">報名成功!</h3>
|
||||
<p className="text-gray-600 mb-4">您的競賽報名已成功提交</p>
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<p className="text-sm text-gray-600 mb-1">申請編號</p>
|
||||
<p className="text-lg font-mono font-semibold text-gray-900">{applicationId}</p>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 space-y-1">
|
||||
<p>• 我們將在 3-5 個工作日內審核您的申請</p>
|
||||
<p>• 審核結果將通過郵件通知您</p>
|
||||
<p>• 如有疑問,請聯繫管理員</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<FileText className="w-12 h-12 text-blue-600 mx-auto mb-3" />
|
||||
<h3 className="text-lg font-semibold">應用資訊</h3>
|
||||
<p className="text-sm text-gray-600">請填寫您要參賽的 AI 應用基本資訊</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="appName">應用名稱 *</Label>
|
||||
<Input
|
||||
id="appName"
|
||||
value={formData.appName}
|
||||
onChange={(e) => handleInputChange("appName", e.target.value)}
|
||||
placeholder="請輸入應用名稱"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="appDescription">應用描述 *</Label>
|
||||
<Textarea
|
||||
id="appDescription"
|
||||
value={formData.appDescription}
|
||||
onChange={(e) => handleInputChange("appDescription", e.target.value)}
|
||||
placeholder="請簡要描述您的應用功能和特色"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="appType">應用類型 *</Label>
|
||||
<Select value={formData.appType} onValueChange={(value) => handleInputChange("appType", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="選擇應用類型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||||
<SelectItem value="數據分析">數據分析</SelectItem>
|
||||
<SelectItem value="自動化工具">自動化工具</SelectItem>
|
||||
<SelectItem value="其他">其他</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="techStack">技術架構 *</Label>
|
||||
<Input
|
||||
id="techStack"
|
||||
value={formData.techStack}
|
||||
onChange={(e) => handleInputChange("techStack", e.target.value)}
|
||||
placeholder="例如:Python, TensorFlow, React, Node.js"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="mainFeatures">主要功能</Label>
|
||||
<Textarea
|
||||
id="mainFeatures"
|
||||
value={formData.mainFeatures}
|
||||
onChange={(e) => handleInputChange("mainFeatures", e.target.value)}
|
||||
placeholder="請列出應用的主要功能特色"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<Users className="w-12 h-12 text-green-600 mx-auto mb-3" />
|
||||
<h3 className="text-lg font-semibold">團隊與聯絡資訊</h3>
|
||||
<p className="text-sm text-gray-600">請提供團隊資料和聯絡方式</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="teamName">團隊名稱 *</Label>
|
||||
<Input
|
||||
id="teamName"
|
||||
value={formData.teamName}
|
||||
onChange={(e) => handleInputChange("teamName", e.target.value)}
|
||||
placeholder="請輸入團隊名稱"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="teamSize">團隊人數</Label>
|
||||
<Select value={formData.teamSize} onValueChange={(value) => handleInputChange("teamSize", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 人</SelectItem>
|
||||
<SelectItem value="2">2 人</SelectItem>
|
||||
<SelectItem value="3">3 人</SelectItem>
|
||||
<SelectItem value="4">4 人</SelectItem>
|
||||
<SelectItem value="5+">5 人以上</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<Label htmlFor="contactName">聯絡人姓名 *</Label>
|
||||
<Input
|
||||
id="contactName"
|
||||
value={formData.contactName}
|
||||
onChange={(e) => handleInputChange("contactName", e.target.value)}
|
||||
placeholder="請輸入聯絡人姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="contactEmail">聯絡人信箱 *</Label>
|
||||
<Input
|
||||
id="contactEmail"
|
||||
type="email"
|
||||
value={formData.contactEmail}
|
||||
onChange={(e) => handleInputChange("contactEmail", e.target.value)}
|
||||
placeholder="請輸入聯絡人信箱"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="contactPhone">聯絡電話</Label>
|
||||
<Input
|
||||
id="contactPhone"
|
||||
value={formData.contactPhone}
|
||||
onChange={(e) => handleInputChange("contactPhone", e.target.value)}
|
||||
placeholder="請輸入聯絡電話"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="department">所屬部門 *</Label>
|
||||
<Select value={formData.department} onValueChange={(value) => handleInputChange("department", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="選擇所屬部門" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||||
<SelectItem value="SBU">SBU</SelectItem>
|
||||
<SelectItem value="其他">其他</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 3:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<Target className="w-12 h-12 text-purple-600 mx-auto mb-3" />
|
||||
<h3 className="text-lg font-semibold">參賽動機與確認</h3>
|
||||
<p className="text-sm text-gray-600">最後一步,請確認參賽資訊</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="motivation">參賽動機 *</Label>
|
||||
<Textarea
|
||||
id="motivation"
|
||||
value={formData.motivation}
|
||||
onChange={(e) => handleInputChange("motivation", e.target.value)}
|
||||
placeholder="請分享您參加此次競賽的動機和期望"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="expectedOutcome">期望成果</Label>
|
||||
<Textarea
|
||||
id="expectedOutcome"
|
||||
value={formData.expectedOutcome}
|
||||
onChange={(e) => handleInputChange("expectedOutcome", e.target.value)}
|
||||
placeholder="您希望通過此次競賽達成什麼目標?"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 競賽資訊確認 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center space-x-2">
|
||||
<Trophy className="w-4 h-4 text-yellow-600" />
|
||||
<span>競賽資訊確認</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">競賽名稱</span>
|
||||
<span className="text-sm font-medium">{currentCompetition?.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">競賽時間</span>
|
||||
<span className="text-sm font-medium">
|
||||
{currentCompetition?.year}年{currentCompetition?.month}月
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">競賽狀態</span>
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||
{currentCompetition?.status === "active" ? "進行中" : "即將開始"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 條款同意 */}
|
||||
<div className="flex items-start space-x-2">
|
||||
<Checkbox
|
||||
id="agreeTerms"
|
||||
checked={formData.agreeTerms}
|
||||
onCheckedChange={(checked) => handleInputChange("agreeTerms", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="agreeTerms" className="text-sm leading-relaxed">
|
||||
我已閱讀並同意競賽規則與條款,確認提交的資訊真實有效,並同意主辦方使用相關資料進行競賽管理和宣傳用途。
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center space-x-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-600" />
|
||||
<span>競賽報名</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{currentCompetition?.name}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!isSubmitted && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
步驟 {currentStep} / {totalSteps}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-h-[400px]">{renderStepContent()}</div>
|
||||
|
||||
{!isSubmitted && (
|
||||
<div className="flex justify-between pt-6 border-t">
|
||||
<Button variant="outline" onClick={handlePrevious} disabled={currentStep === 1}>
|
||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
||||
上一步
|
||||
</Button>
|
||||
|
||||
{currentStep < totalSteps ? (
|
||||
<Button onClick={handleNext} disabled={!validateStep(currentStep)}>
|
||||
下一步
|
||||
<ChevronRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!validateStep(3) || isSubmitting}
|
||||
className="bg-gradient-to-r from-orange-600 to-yellow-600 hover:from-orange-700 hover:to-yellow-700"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
提交中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Award className="w-4 h-4 mr-2" />
|
||||
提交報名
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSubmitted && (
|
||||
<div className="flex justify-center pt-6 border-t">
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
325
components/competition/team-detail-dialog.tsx
Normal file
325
components/competition/team-detail-dialog.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import {
|
||||
Users,
|
||||
Mail,
|
||||
Eye,
|
||||
Heart,
|
||||
Trophy,
|
||||
Star,
|
||||
MessageSquare,
|
||||
ImageIcon,
|
||||
Mic,
|
||||
TrendingUp,
|
||||
Brain,
|
||||
Zap,
|
||||
ExternalLink,
|
||||
} from "lucide-react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import { LikeButton } from "@/components/like-button"
|
||||
import { AppDetailDialog } from "@/components/app-detail-dialog"
|
||||
|
||||
interface TeamDetailDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
team: any
|
||||
}
|
||||
|
||||
// App data for team apps - empty for production
|
||||
const getAppDetails = (appId: string) => {
|
||||
return {
|
||||
id: appId,
|
||||
name: "",
|
||||
type: "",
|
||||
description: "",
|
||||
icon: null,
|
||||
fullDescription: "",
|
||||
features: [],
|
||||
author: "",
|
||||
category: "",
|
||||
tags: [],
|
||||
demoUrl: "",
|
||||
sourceUrl: "",
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors = {
|
||||
文字處理: "bg-blue-100 text-blue-800 border-blue-200",
|
||||
圖像生成: "bg-purple-100 text-purple-800 border-purple-200",
|
||||
語音辨識: "bg-green-100 text-green-800 border-green-200",
|
||||
推薦系統: "bg-orange-100 text-orange-800 border-orange-200",
|
||||
}
|
||||
return colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800 border-gray-200"
|
||||
}
|
||||
|
||||
export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogProps) {
|
||||
const { getLikeCount, getViewCount, getAppRating } = useAuth()
|
||||
const [selectedTab, setSelectedTab] = useState("overview")
|
||||
const [selectedApp, setSelectedApp] = useState<any>(null)
|
||||
const [appDetailOpen, setAppDetailOpen] = useState(false)
|
||||
|
||||
if (!team) return null
|
||||
|
||||
const leader = team.members.find((m: any) => m.id === team.leader)
|
||||
|
||||
const handleAppClick = (appId: string) => {
|
||||
const appDetails = getAppDetails(appId)
|
||||
// Create app object that matches AppDetailDialog interface
|
||||
const app = {
|
||||
id: Number.parseInt(appId),
|
||||
name: appDetails.name,
|
||||
type: appDetails.type,
|
||||
department: team.department, // Use team's department
|
||||
description: appDetails.description,
|
||||
icon: appDetails.icon,
|
||||
creator: appDetails.author,
|
||||
featured: false,
|
||||
judgeScore: 0,
|
||||
}
|
||||
setSelectedApp(app)
|
||||
setAppDetailOpen(true)
|
||||
}
|
||||
|
||||
const totalLikes = team.apps.reduce((sum: number, appId: string) => sum + getLikeCount(appId), 0)
|
||||
const totalViews = team.apps.reduce((sum: number, appId: string) => sum + getViewCount(appId), 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center space-x-2">
|
||||
<Users className="w-5 h-5 text-green-500" />
|
||||
<span>{team.name}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>團隊詳細資訊與成員介紹</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Team Overview */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">團隊概覽</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-green-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">{team.members.length}</div>
|
||||
<div className="text-sm text-gray-600">團隊成員</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">{team.apps.length}</div>
|
||||
<div className="text-sm text-gray-600">提交應用</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-red-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">{totalLikes}</div>
|
||||
<div className="text-sm text-gray-600">總按讚數</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-purple-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600">{totalViews}</div>
|
||||
<div className="text-sm text-gray-600">總瀏覽數</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg">
|
||||
<Button
|
||||
variant={selectedTab === "overview" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedTab("overview")}
|
||||
className="flex-1"
|
||||
>
|
||||
團隊資訊
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedTab === "members" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedTab("members")}
|
||||
className="flex-1"
|
||||
>
|
||||
成員介紹
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedTab === "apps" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedTab("apps")}
|
||||
className="flex-1"
|
||||
>
|
||||
提交應用
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{selectedTab === "overview" && (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">基本資訊</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">團隊名稱</label>
|
||||
<p className="text-gray-900">{team.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">代表部門</label>
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{team.department}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">團隊隊長</label>
|
||||
<p className="text-gray-900">{leader?.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">聯絡信箱</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Mail className="w-4 h-4 text-gray-500" />
|
||||
<p className="text-gray-900">{team.contactEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">競賽表現</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<Trophy className="w-8 h-8 text-yellow-500 mx-auto mb-2" />
|
||||
<div className="text-lg font-bold">{team.popularityScore}</div>
|
||||
<div className="text-sm text-gray-600">人氣指數</div>
|
||||
</div>
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<Eye className="w-8 h-8 text-blue-500 mx-auto mb-2" />
|
||||
<div className="text-lg font-bold">{totalViews}</div>
|
||||
<div className="text-sm text-gray-600">總瀏覽數</div>
|
||||
</div>
|
||||
<div className="text-center p-4 border rounded-lg">
|
||||
<Heart className="w-8 h-8 text-red-500 mx-auto mb-2" />
|
||||
<div className="text-lg font-bold">{totalLikes}</div>
|
||||
<div className="text-sm text-gray-600">總按讚數</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTab === "members" && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">團隊成員 ({team.members.length}人)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{team.members.map((member: any, index: number) => (
|
||||
<div key={member.id} className="flex items-center space-x-3 p-4 border rounded-lg">
|
||||
<Avatar className="w-12 h-12">
|
||||
<AvatarImage src={`/placeholder-40x40.png`} />
|
||||
<AvatarFallback className="bg-green-100 text-green-700 font-medium">
|
||||
{member.name[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="font-medium">{member.name}</h4>
|
||||
{member.id === team.leader && (
|
||||
<Badge variant="default" className="bg-yellow-100 text-yellow-800 text-xs">
|
||||
隊長
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">{member.role}</p>
|
||||
<p className="text-xs text-gray-500">{member.department}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{selectedTab === "apps" && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">提交應用 ({team.apps.length}個)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{team.apps.map((appId: string) => {
|
||||
const app = getAppDetails(appId)
|
||||
const IconComponent = app.icon
|
||||
const likes = getLikeCount(appId)
|
||||
const views = getViewCount(appId)
|
||||
const rating = getAppRating(appId)
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={appId}
|
||||
className="hover:shadow-md transition-all duration-200 cursor-pointer group"
|
||||
onClick={() => handleAppClick(appId)}
|
||||
>
|
||||
<CardContent className="p-4 flex flex-col h-full">
|
||||
<div className="flex items-start space-x-3 mb-3">
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center group-hover:bg-blue-100 transition-colors">
|
||||
<IconComponent className="w-5 h-5 text-gray-600 group-hover:text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="font-medium text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
{app.name}
|
||||
</h4>
|
||||
<ExternalLink className="w-3 h-3 text-gray-400 group-hover:text-blue-500 opacity-0 group-hover:opacity-100 transition-all" />
|
||||
</div>
|
||||
<Badge variant="outline" className={`${getTypeColor(app.type)} text-xs mt-1`}>
|
||||
{app.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{app.description}</p>
|
||||
<div className="flex items-center justify-between mt-auto">
|
||||
<div className="flex items-center space-x-3 text-xs text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="w-3 h-3" />
|
||||
<span>{views}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="w-3 h-3 text-yellow-500" />
|
||||
<span>{rating.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<LikeButton appId={appId} size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* App Detail Dialog */}
|
||||
{selectedApp && <AppDetailDialog open={appDetailOpen} onOpenChange={setAppDetailOpen} app={selectedApp} />}
|
||||
</>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user