建立檔案

This commit is contained in:
2025-08-05 08:22:44 +08:00
commit 042d03aff7
122 changed files with 34763 additions and 0 deletions

572
app/competition/page.tsx Normal file
View File

@@ -0,0 +1,572 @@
"use client"
import { useState } from "react"
import { useAuth } from "@/contexts/auth-context"
import { useCompetition } from "@/contexts/competition-context"
import { Trophy, Award, Medal, Target, Users, Lightbulb, ArrowLeft, Plus, Search, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Input } from "@/components/ui/input"
import { PopularityRankings } from "@/components/competition/popularity-rankings"
import { CompetitionDetailDialog } from "@/components/competition/competition-detail-dialog"
import { AwardDetailDialog } from "@/components/competition/award-detail-dialog"
export default function CompetitionPage() {
const { user, canAccessAdmin } = useAuth()
const { competitions, awards, getAwardsByYear, getCompetitionRankings } = useCompetition()
const [selectedCompetitionTypeFilter, setSelectedCompetitionTypeFilter] = useState("all")
const [selectedMonthFilter, setSelectedMonthFilter] = useState("all")
const [selectedAwardCategory, setSelectedAwardCategory] = useState("all")
const [selectedYear, setSelectedYear] = useState(2024)
const [searchQuery, setSearchQuery] = useState("")
const [showCompetitionDetail, setShowCompetitionDetail] = useState(false)
const [selectedRanking, setSelectedRanking] = useState<any>(null)
const [selectedCompetitionType, setSelectedCompetitionType] = useState<"individual" | "team" | "proposal">(
"individual",
)
const [showAwardDetail, setShowAwardDetail] = useState(false)
const [selectedAward, setSelectedAward] = useState<any>(null)
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 handleShowCompetitionDetail = (ranking: any, type: "individual" | "team" | "proposal") => {
setSelectedRanking(ranking)
setSelectedCompetitionType(type)
setShowCompetitionDetail(true)
}
const handleShowAwardDetail = (award: any) => {
setSelectedAward(award)
setShowAwardDetail(true)
}
const getFilteredAwards = () => {
let filteredAwards = getAwardsByYear(selectedYear)
// 搜索功能 - 按应用名称、创作者或奖项名称搜索
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim()
filteredAwards = filteredAwards.filter((award) => {
return (
award.appName?.toLowerCase().includes(query) ||
award.creator?.toLowerCase().includes(query) ||
award.awardName?.toLowerCase().includes(query)
)
})
}
if (selectedCompetitionTypeFilter !== "all") {
filteredAwards = filteredAwards.filter((award) => award.competitionType === selectedCompetitionTypeFilter)
}
if (selectedMonthFilter !== "all") {
filteredAwards = filteredAwards.filter((award) => award.month === Number.parseInt(selectedMonthFilter))
}
if (selectedAwardCategory !== "all") {
if (selectedAwardCategory === "ranking") {
filteredAwards = filteredAwards.filter((award) => award.rank > 0 && award.rank <= 3)
} else if (selectedAwardCategory === "popular") {
filteredAwards = filteredAwards.filter((award) => award.awardType === "popular")
} else {
filteredAwards = filteredAwards.filter((award) => award.category === selectedAwardCategory)
}
}
return filteredAwards.sort((a, b) => {
// Sort by month first, then by rank
if (a.month !== b.month) return b.month - a.month
if (a.rank !== b.rank) {
if (a.rank === 0) return 1
if (b.rank === 0) return -1
return a.rank - b.rank
}
return 0
})
}
const filteredAwards = getFilteredAwards()
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
{/* Header */}
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="sm"
onClick={() => window.history.back()}
className="text-gray-700 hover:text-blue-600 hover:bg-blue-50"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
<Trophy className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-gray-900"></h1>
<p className="text-xs text-gray-500">COMPETITION CENTER</p>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
</div>
</div>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Hero Section */}
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">AI </h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto mb-8">
AI 耀
</p>
</div>
<Tabs defaultValue="rankings" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-8">
<TabsTrigger value="rankings" className="flex items-center space-x-2">
<Trophy className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="awards" className="flex items-center space-x-2">
<Award className="w-4 h-4" />
<span></span>
</TabsTrigger>
</TabsList>
<TabsContent value="rankings">
<PopularityRankings />
</TabsContent>
<TabsContent value="awards">
<div className="space-y-8">
{/* Enhanced Filter Section */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center space-x-2">
<Medal className="w-5 h-5 text-purple-500" />
<span></span>
</CardTitle>
<div className="flex items-center space-x-3">
<Select
value={selectedYear.toString()}
onValueChange={(value) => setSelectedYear(Number.parseInt(value))}
>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2023">2023</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-gray-600"> {selectedYear} </p>
{searchQuery && (
<div className="text-sm text-blue-600 bg-blue-50 px-3 py-1 rounded-full">
{searchQuery}
</div>
)}
</div>
{/* Search and Filter Controls */}
<div className="space-y-4">
{/* Search Bar */}
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-4 w-4 text-gray-400" />
</div>
<Input
type="text"
placeholder="搜尋應用名稱、創作者或獎項名稱..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-10 w-full md:w-96"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
>
<X className="h-4 w-4" />
</button>
)}
</div>
{/* Filter Controls */}
<div className="flex flex-wrap gap-4 items-center">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={selectedCompetitionTypeFilter} onValueChange={setSelectedCompetitionTypeFilter}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="individual"></SelectItem>
<SelectItem value="team"></SelectItem>
<SelectItem value="proposal"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={selectedMonthFilter} onValueChange={setSelectedMonthFilter}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<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>
<SelectItem value="6">6</SelectItem>
<SelectItem value="7">7</SelectItem>
<SelectItem value="8">8</SelectItem>
<SelectItem value="9">9</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={selectedAwardCategory} onValueChange={setSelectedAwardCategory}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="ranking"></SelectItem>
<SelectItem value="popular"></SelectItem>
<SelectItem value="innovation"></SelectItem>
<SelectItem value="technical"></SelectItem>
<SelectItem value="practical"></SelectItem>
</SelectContent>
</Select>
</div>
{/* Clear Filters Button */}
{(searchQuery || selectedCompetitionTypeFilter !== "all" || selectedMonthFilter !== "all" || selectedAwardCategory !== "all") && (
<div className="flex items-center">
<Button
variant="outline"
size="sm"
onClick={() => {
setSearchQuery("")
setSelectedCompetitionTypeFilter("all")
setSelectedMonthFilter("all")
setSelectedAwardCategory("all")
}}
className="text-gray-600 hover:text-gray-800"
>
<X className="w-4 h-4 mr-1" />
</Button>
</div>
)}
</div>
</div>
{/* Statistics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
<div className="text-center p-3 bg-blue-50 rounded-lg">
<div className="text-lg font-bold text-blue-600">{filteredAwards.length}</div>
<div className="text-xs text-blue-600"></div>
</div>
<div className="text-center p-3 bg-yellow-50 rounded-lg">
<div className="text-lg font-bold text-yellow-600">
{filteredAwards.filter((a) => a.rank > 0 && a.rank <= 3).length}
</div>
<div className="text-xs text-yellow-600"></div>
</div>
<div className="text-center p-3 bg-red-50 rounded-lg">
<div className="text-lg font-bold text-red-600">
{filteredAwards.filter((a) => a.awardType === "popular").length}
</div>
<div className="text-xs text-red-600"></div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-lg font-bold text-green-600">
{new Set(filteredAwards.map((a) => `${a.year}-${a.month}`)).size}
</div>
<div className="text-xs text-green-600"></div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Awards Grid with Enhanced Display */}
{filteredAwards.length > 0 ? (
<div className="space-y-8">
{/* Group awards by month */}
{Array.from(new Set(filteredAwards.map((award) => award.month)))
.sort((a, b) => b - a)
.map((month) => {
const monthAwards = filteredAwards.filter((award) => award.month === month)
const competition = competitions.find((c) => c.month === month && c.year === selectedYear)
return (
<div key={month} className="space-y-4">
<div className="flex items-center space-x-4">
<h3 className="text-xl font-bold text-gray-900">
{selectedYear}{month}
</h3>
{competition && (
<Badge variant="outline" className={getCompetitionTypeColor(competition.type)}>
{getCompetitionTypeIcon(competition.type)}
<span className="ml-1">{getCompetitionTypeText(competition.type)}</span>
</Badge>
)}
<Badge variant="secondary" className="bg-gray-100 text-gray-700">
{monthAwards.length}
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{monthAwards.map((award) => (
<Card
key={award.id}
className="relative overflow-hidden border-0 shadow-lg bg-gradient-to-br from-white to-gray-50 hover:shadow-xl transition-shadow cursor-pointer"
onClick={() => handleShowAwardDetail(award)}
>
{/* Rank Badge */}
{award.rank > 0 && award.rank <= 3 && (
<div className="absolute top-2 left-2 z-10">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold text-white ${
award.rank === 1
? "bg-yellow-500"
: award.rank === 2
? "bg-gray-400"
: award.rank === 3
? "bg-orange-600"
: ""
}`}
>
{award.rank}
</div>
</div>
)}
<div className="absolute top-4 right-4 text-3xl">{award.icon}</div>
<CardHeader className="pb-3 pt-12">
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
<Badge
variant="secondary"
className={`w-fit ${
award.awardType === "popular"
? "bg-red-100 text-red-800 border-red-200"
: award.rank === 1
? "bg-yellow-100 text-yellow-800 border-yellow-200"
: award.rank === 2
? "bg-gray-100 text-gray-800 border-gray-200"
: award.rank === 3
? "bg-orange-100 text-orange-800 border-orange-200"
: "bg-blue-100 text-blue-800 border-blue-200"
}`}
>
{award.awardName}
</Badge>
<Badge
variant="outline"
className={getCompetitionTypeColor(award.competitionType)}
>
{getCompetitionTypeIcon(award.competitionType)}
<span className="ml-1">{getCompetitionTypeText(award.competitionType)}</span>
</Badge>
</div>
<CardTitle className="text-lg line-clamp-2">
{award.appName || award.proposalTitle || award.teamName}
</CardTitle>
<p className="text-sm text-gray-500">by {award.creator}</p>
<div className="text-xs text-gray-400">
{award.year}{award.month}
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">
{award.competitionType === "proposal"
? "評審評分"
: award.awardType === "popular"
? award.competitionType === "team"
? "人氣指數"
: "收藏數"
: "評審評分"}
</span>
<span className="font-bold text-lg text-gray-900">
{award.awardType === "popular" && award.competitionType === "team"
? `${award.score}`
: award.awardType === "popular"
? `${award.score}`
: award.score}
</span>
</div>
<Button
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
onClick={(e) => {
e.stopPropagation()
handleShowAwardDetail(award)
}}
>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
})}
</div>
) : (
<Card>
<CardContent className="text-center py-12">
<div className="space-y-4">
{searchQuery ? (
<Search className="w-16 h-16 text-gray-400 mx-auto" />
) : (
<Medal className="w-16 h-16 text-gray-400 mx-auto" />
)}
<div>
<h3 className="text-xl font-semibold text-gray-600 mb-2">
{searchQuery ? (
<>{searchQuery}</>
) : (
<>
{selectedYear}{selectedMonthFilter !== "all" ? `${selectedMonthFilter}` : ""}
</>
)}
</h3>
<p className="text-gray-500">
{searchQuery
? "嘗試使用其他關鍵字或調整篩選條件"
: "請調整篩選條件查看其他得獎作品"}
</p>
</div>
<div className="flex justify-center gap-2">
<Button
variant="outline"
className="bg-transparent"
onClick={() => {
setSearchQuery("")
setSelectedCompetitionTypeFilter("all")
setSelectedMonthFilter("all")
setSelectedAwardCategory("all")
}}
>
<X className="w-4 h-4 mr-1" />
</Button>
{searchQuery && (
<Button
variant="outline"
className="bg-transparent"
onClick={() => setSearchQuery("")}
>
</Button>
)}
</div>
</div>
</CardContent>
</Card>
)}
</div>
</TabsContent>
</Tabs>
{/* Competition Detail Dialog */}
{selectedRanking && (
<CompetitionDetailDialog
open={showCompetitionDetail}
onOpenChange={setShowCompetitionDetail}
ranking={selectedRanking}
competitionType={selectedCompetitionType}
/>
)}
{/* Award Detail Dialog */}
{selectedAward && (
<AwardDetailDialog open={showAwardDetail} onOpenChange={setShowAwardDetail} award={selectedAward} />
)}
</div>
</div>
)
}