新增競賽前台呈現、刪除競賽、修改競賽狀態
This commit is contained in:
@@ -150,6 +150,67 @@ export function CompetitionManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取當前競賽
|
||||
const fetchCurrentCompetition = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/competitions/current')
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setCurrentCompetition(data.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('獲取當前競賽失敗:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 設置當前競賽
|
||||
const setCurrentCompetitionInDb = async (competitionId: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/competitions/current', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ competitionId }),
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setCurrentCompetition(data.data)
|
||||
setSuccess('當前競賽設置成功!')
|
||||
setTimeout(() => setSuccess(''), 3000)
|
||||
} else {
|
||||
setError('設置當前競賽失敗: ' + data.message)
|
||||
setTimeout(() => setError(''), 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('設置當前競賽失敗:', error)
|
||||
setError('設置當前競賽失敗')
|
||||
setTimeout(() => setError(''), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消當前競賽
|
||||
const clearCurrentCompetitionInDb = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/competitions/current', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setCurrentCompetition(null)
|
||||
setSuccess('當前競賽已取消!')
|
||||
setTimeout(() => setSuccess(''), 3000)
|
||||
} else {
|
||||
setError('取消當前競賽失敗: ' + data.message)
|
||||
setTimeout(() => setError(''), 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消當前競賽失敗:', error)
|
||||
setError('取消當前競賽失敗')
|
||||
setTimeout(() => setError(''), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCompetitionStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/competitions/stats')
|
||||
@@ -630,6 +691,7 @@ export function CompetitionManagement() {
|
||||
// 組件載入時獲取資料
|
||||
useEffect(() => {
|
||||
fetchCompetitions()
|
||||
fetchCurrentCompetition()
|
||||
fetchCompetitionStats()
|
||||
fetchJudges()
|
||||
fetchJudgeStats()
|
||||
@@ -1770,22 +1832,50 @@ export function CompetitionManagement() {
|
||||
if (!selectedCompetitionForAction) return
|
||||
|
||||
setIsLoading(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
|
||||
try {
|
||||
// 調用 API 刪除競賽
|
||||
const response = await fetch(`/api/admin/competitions/${selectedCompetitionForAction.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
// 同時從 context 中刪除
|
||||
deleteCompetition(selectedCompetitionForAction.id)
|
||||
setShowDeleteCompetitionConfirm(false)
|
||||
setSelectedCompetitionForAction(null)
|
||||
setSuccess("競賽刪除成功!")
|
||||
|
||||
// 重新載入競賽列表
|
||||
await fetchCompetitions()
|
||||
} else {
|
||||
setError("競賽刪除失敗: " + data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刪除競賽失敗:', error)
|
||||
setError("競賽刪除失敗")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setTimeout(() => setError(""), 3000)
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateStatus = async () => {
|
||||
if (!selectedCompetitionForAction) return
|
||||
|
||||
setIsLoading(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
|
||||
try {
|
||||
// 更新資料庫
|
||||
const updatedCompetition = await updateCompetitionInDb(selectedCompetitionForAction.id, {
|
||||
status: newStatus,
|
||||
})
|
||||
|
||||
if (updatedCompetition) {
|
||||
// 同時更新 context
|
||||
updateCompetition(selectedCompetitionForAction.id, {
|
||||
...selectedCompetitionForAction,
|
||||
status: newStatus,
|
||||
@@ -1794,8 +1884,16 @@ export function CompetitionManagement() {
|
||||
setShowChangeStatusDialog(false)
|
||||
setSelectedCompetitionForAction(null)
|
||||
setSuccess("競賽狀態更新成功!")
|
||||
} else {
|
||||
setError("競賽狀態更新失敗")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新競賽狀態失敗:', error)
|
||||
setError("競賽狀態更新失敗")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
setTimeout(() => setError(""), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const getCompetitionTypeIcon = (type: string) => {
|
||||
@@ -1918,6 +2016,8 @@ export function CompetitionManagement() {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "bg-green-100 text-green-800 border-green-200"
|
||||
case "ongoing":
|
||||
return "bg-yellow-100 text-yellow-800 border-yellow-200"
|
||||
case "active":
|
||||
return "bg-blue-100 text-blue-800 border-blue-200"
|
||||
case "judging":
|
||||
@@ -1933,6 +2033,7 @@ export function CompetitionManagement() {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "已完成"
|
||||
case "ongoing":
|
||||
case "active":
|
||||
return "進行中"
|
||||
case "judging":
|
||||
@@ -2079,7 +2180,7 @@ export function CompetitionManagement() {
|
||||
{isLoadingDb ? (
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
) : (
|
||||
dbStats?.active || displayCompetitions.filter((c) => c.status === "active").length
|
||||
dbStats?.active || displayCompetitions.filter((c) => c.status === "active" || c.status === "ongoing").length
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -2245,14 +2346,14 @@ export function CompetitionManagement() {
|
||||
</DropdownMenuItem>
|
||||
|
||||
{!isCurrentCompetition && (
|
||||
<DropdownMenuItem onClick={() => setCurrentCompetition(competition)}>
|
||||
<DropdownMenuItem onClick={() => setCurrentCompetitionInDb(competition.id)}>
|
||||
<Star className="w-4 h-4 mr-2" />
|
||||
設為當前競賽
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{isCurrentCompetition && (
|
||||
<DropdownMenuItem onClick={() => setCurrentCompetition(null)}>
|
||||
<DropdownMenuItem onClick={() => clearCurrentCompetitionInDb()}>
|
||||
<StarOff className="w-4 h-4 mr-2" />
|
||||
取消當前競賽
|
||||
</DropdownMenuItem>
|
||||
@@ -5395,9 +5496,9 @@ export function CompetitionManagement() {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>當前狀態</Label>
|
||||
<p className="text-sm text-gray-600">
|
||||
<div className="text-sm text-gray-600">
|
||||
<Badge>{getStatusText(selectedCompetitionForAction.status)}</Badge>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">新狀態</Label>
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user