新增競賽前台呈現、刪除競賽、修改競賽狀態

This commit is contained in:
2025-09-16 14:57:40 +08:00
parent 1f2fb14bd0
commit b4386dc481
21 changed files with 1714 additions and 127 deletions

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { useAuth } from "@/contexts/auth-context"
import { useCompetition } from "@/contexts/competition-context"
import {
@@ -55,10 +55,67 @@ export function PopularityRankings() {
const [individualCurrentPage, setIndividualCurrentPage] = useState(0)
const [teamCurrentPage, setTeamCurrentPage] = useState(0)
// 新增狀態
const [competitionApps, setCompetitionApps] = useState<any[]>([])
const [competitionTeams, setCompetitionTeams] = useState<any[]>([])
const [competitionJudges, setCompetitionJudges] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(false)
const ITEMS_PER_PAGE = 3
// 載入當前競賽的數據
useEffect(() => {
if (currentCompetition) {
loadCompetitionData(currentCompetition.id)
} else {
// 如果沒有當前競賽,清空數據
setCompetitionApps([])
setCompetitionTeams([])
setCompetitionJudges([])
}
}, [currentCompetition])
const loadCompetitionData = async (competitionId: string) => {
setIsLoading(true)
try {
// 並行載入競賽的應用、團隊和評審數據
const [appsResponse, teamsResponse, judgesResponse] = await Promise.all([
fetch(`/api/competitions/${competitionId}/apps`), // 移除 competitionType 參數,載入所有應用
fetch(`/api/competitions/${competitionId}/teams`),
fetch(`/api/competitions/${competitionId}/judges`)
])
const [appsData, teamsData, judgesData] = await Promise.all([
appsResponse.json(),
teamsResponse.json(),
judgesResponse.json()
])
if (appsData.success) {
// 合併個人應用和團隊應用
const allApps = appsData.data.apps || []
console.log('📱 載入的應用數據:', allApps)
setCompetitionApps(allApps)
}
if (teamsData.success) {
const teams = teamsData.data.teams || []
console.log('👥 載入的團隊數據:', teams)
setCompetitionTeams(teams)
}
if (judgesData.success) {
const judges = judgesData.data.judges || []
console.log('👨‍⚖️ 載入的評審數據:', judges)
setCompetitionJudges(judges)
}
} catch (error) {
console.error('載入競賽數據失敗:', error)
} finally {
setIsLoading(false)
}
}
// Filter apps based on search criteria
const filteredApps = aiApps.filter((app) => {
const filteredApps = competitionApps.filter((app) => {
const matchesSearch =
app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -72,9 +129,7 @@ export function PopularityRankings() {
// 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
return (b.likes || 0) - (a.likes || 0)
})
// Group apps by competition type
@@ -319,23 +374,8 @@ export function PopularityRankings() {
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)
// 團隊已經從 API 獲取了人氣分數,直接使用
const teamsWithScores = teams.sort((a, b) => b.popularityScore - a.popularityScore)
const currentPage = teamCurrentPage
const setCurrentPage = setTeamCurrentPage
@@ -512,7 +552,7 @@ export function PopularityRankings() {
<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)}
{team.totalLikes || 0}
</span>
</div>
<Button
@@ -593,7 +633,11 @@ export function PopularityRankings() {
</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>
<span className="text-sm">
{currentCompetition.type === 'team'
? `${competitionTeams.length} 個參賽團隊`
: `${filteredApps.length} 個參賽應用`}
</span>
</div>
<div className="flex items-center space-x-2">
<Badge
@@ -633,27 +677,39 @@ export function PopularityRankings() {
<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>
))}
{isLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto"></div>
<p className="mt-2 text-gray-600">...</p>
</div>
) : competitionJudges.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{competitionJudges.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>
))}
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<Crown className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p></p>
</div>
)}
</CardContent>
</Card>
@@ -733,23 +789,30 @@ export function PopularityRankings() {
{/* Team Competition Section */}
{(selectedCompetitionType === "all" || selectedCompetitionType === "team") &&
currentCompetition?.type === "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,
competitionTeams.filter(
(team) => {
const matchesSearch = searchTerm === "" ||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
team.members?.some((member: any) => member.name.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesDepartment = selectedDepartment === "all" || team.department === selectedDepartment;
return matchesSearch && matchesDepartment;
}
),
"團隊賽",
)}
{/* No Results */}
{filteredApps.length === 0 && (
{filteredApps.length === 0 && competitionTeams.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>
<h3 className="text-xl font-semibold text-gray-600 mb-2">
{currentCompetition?.type === 'team' ? '沒有找到符合條件的團隊' : '沒有找到符合條件的應用'}
</h3>
<p className="text-gray-500">調</p>
<Button
variant="outline"

View File

@@ -20,6 +20,13 @@ import {
Brain,
Zap,
ExternalLink,
Bot,
Code,
Database,
Palette,
Volume2,
Search,
BarChart3,
} from "lucide-react"
import { useAuth } from "@/contexts/auth-context"
import { LikeButton } from "@/components/like-button"
@@ -31,22 +38,68 @@ interface TeamDetailDialogProps {
team: any
}
// App data for team apps - empty for production
const getAppDetails = (appId: string) => {
// 圖標映射函數
const getIconComponent = (iconName: string) => {
const iconMap: { [key: string]: any } = {
'Brain': Brain,
'Bot': Bot,
'Code': Code,
'Database': Database,
'Palette': Palette,
'Volume2': Volume2,
'Search': Search,
'BarChart3': BarChart3,
'Mic': Mic,
'ImageIcon': ImageIcon,
'MessageSquare': MessageSquare,
'Zap': Zap,
'TrendingUp': TrendingUp,
};
return iconMap[iconName] || Brain;
}
// App data for team apps - get from team data
const getAppDetails = (appId: string, team: any) => {
const appDetail = team.appsDetails?.find((app: any) => app.id === appId);
if (appDetail) {
return {
id: appDetail.id,
name: appDetail.name || "未命名應用",
type: appDetail.type || "未知類型",
description: appDetail.description || "無描述",
icon: getIconComponent(appDetail.icon) || Brain,
fullDescription: appDetail.description || "無描述",
features: [],
author: appDetail.creator_name || "未知作者",
category: appDetail.category || "未分類",
tags: [],
demoUrl: "",
sourceUrl: "",
likes: appDetail.likes_count || 0,
views: appDetail.views_count || 0,
rating: Number(appDetail.rating) || 0
};
}
return {
id: appId,
name: "",
type: "",
description: "",
icon: null,
fullDescription: "",
name: "未命名應用",
type: "未知類型",
description: "無描述",
icon: Brain,
fullDescription: "無描述",
features: [],
author: "",
category: "",
author: "未知作者",
category: "未分類",
tags: [],
demoUrl: "",
sourceUrl: "",
}
likes: 0,
views: 0,
rating: 0
};
}
const getTypeColor = (type: string) => {
@@ -67,10 +120,10 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
if (!team) return null
const leader = team.members.find((m: any) => m.id === team.leader)
const leader = team.members.find((m: any) => m.user_id === team.leader)
const handleAppClick = (appId: string) => {
const appDetails = getAppDetails(appId)
const appDetails = getAppDetails(appId, team)
// Create app object that matches AppDetailDialog interface
const app = {
id: Number.parseInt(appId),
@@ -81,14 +134,17 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
icon: appDetails.icon,
creator: appDetails.author,
featured: false,
judgeScore: 0,
judgeScore: appDetails.rating || 0,
likes: appDetails.likes || 0,
views: appDetails.views || 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)
// 使用從數據庫獲取的真實數據
const totalLikes = team.totalLikes || 0
const totalViews = team.totalViews || 0
return (
<>
@@ -179,13 +235,13 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
</div>
<div>
<label className="text-sm font-medium text-gray-700"></label>
<p className="text-gray-900">{leader?.name}</p>
<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>
<p className="text-gray-900">{team.contact_email || '未提供'}</p>
</div>
</div>
</div>
@@ -261,11 +317,12 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
<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)
const app = getAppDetails(appId, team)
// 如果沒有圖標,使用默認的 Brain 圖標
const IconComponent = app.icon || Brain
const likes = app.likes || 0
const views = app.views || 0
const rating = app.rating || 0
return (
<Card
@@ -299,11 +356,16 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
</div>
<div className="flex items-center space-x-1">
<Star className="w-3 h-3 text-yellow-500" />
<span>{rating.toFixed(1)}</span>
<span>{Number(rating).toFixed(1)}</span>
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
<LikeButton appId={appId} size="sm" />
<LikeButton
appId={appId}
size="sm"
likeCount={likes}
showCount={true}
/>
</div>
</div>
</CardContent>