429 lines
17 KiB
TypeScript
429 lines
17 KiB
TypeScript
"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,
|
||
Bot,
|
||
Code,
|
||
Database,
|
||
Palette,
|
||
Volume2,
|
||
Search,
|
||
BarChart3,
|
||
Camera,
|
||
Smartphone,
|
||
Monitor,
|
||
Globe,
|
||
FileText,
|
||
} 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
|
||
}
|
||
|
||
// 圖標映射函數
|
||
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,
|
||
'Camera': Camera,
|
||
'Smartphone': Smartphone,
|
||
'Monitor': Monitor,
|
||
'Globe': Globe,
|
||
'FileText': FileText,
|
||
};
|
||
|
||
return iconMap[iconName] || Bot;
|
||
}
|
||
|
||
// App data for team apps - get from team data
|
||
const getAppDetails = (appId: string, team: any) => {
|
||
// 首先嘗試從 appsDetails 獲取
|
||
let appDetail = team.appsDetails?.find((app: any) => app.id === appId);
|
||
|
||
// 如果沒有找到,嘗試從 apps 獲取
|
||
if (!appDetail) {
|
||
appDetail = team.apps?.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,
|
||
iconColor: appDetail.icon_color || "from-blue-500 to-purple-500",
|
||
fullDescription: appDetail.description || "無描述",
|
||
features: [],
|
||
author: appDetail.creator_name || "未知作者",
|
||
department: appDetail.creator_department || "未知部門",
|
||
category: appDetail.category || "未分類",
|
||
tags: [],
|
||
demoUrl: "",
|
||
sourceUrl: "",
|
||
likes: appDetail.likes_count || 0,
|
||
views: appDetail.views_count || 0,
|
||
rating: Number(appDetail.rating) || 0,
|
||
createdAt: appDetail.created_at
|
||
};
|
||
}
|
||
|
||
return {
|
||
id: appId,
|
||
name: "未命名應用",
|
||
type: "未知類型",
|
||
description: "無描述",
|
||
icon: Brain,
|
||
fullDescription: "無描述",
|
||
features: [],
|
||
author: "未知作者",
|
||
category: "未分類",
|
||
tags: [],
|
||
demoUrl: "",
|
||
sourceUrl: "",
|
||
likes: 0,
|
||
views: 0,
|
||
rating: 0
|
||
};
|
||
}
|
||
|
||
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.user_id === team.leader)
|
||
|
||
const handleAppClick = (appId: string) => {
|
||
const appDetails = getAppDetails(appId, team)
|
||
// Create app object that matches AppDetailDialog interface
|
||
const app = {
|
||
id: appId, // 保持為字符串,API 期望字符串 ID
|
||
name: appDetails.name,
|
||
type: appDetails.type,
|
||
department: appDetails.department, // Use app's creator department
|
||
description: appDetails.description,
|
||
icon: appDetails.icon,
|
||
iconColor: appDetails.iconColor, // Pass icon color
|
||
creator: appDetails.author,
|
||
featured: false,
|
||
judgeScore: appDetails.rating || 0,
|
||
// 使用 AppDetailDialog 期望的屬性名
|
||
likesCount: appDetails.likes || 0,
|
||
viewsCount: appDetails.views || 0,
|
||
rating: appDetails.rating || 0,
|
||
reviewsCount: 0, // 可以從 API 獲取,暫時設為 0
|
||
createdAt: appDetails.createdAt, // Pass creation date
|
||
}
|
||
setSelectedApp(app)
|
||
setAppDetailOpen(true)
|
||
}
|
||
|
||
// 使用從數據庫獲取的真實數據
|
||
const totalLikes = team.totalLikes || 0
|
||
const totalViews = team.totalViews || 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.contact_email || '未提供'}</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={member.avatar} />
|
||
<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.role === '隊長' && (
|
||
<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((app: any) => {
|
||
// 如果 app 是字符串 ID,使用 getAppDetails 獲取詳情
|
||
// 如果 app 是對象,直接使用
|
||
const appData = typeof app === 'string' ? getAppDetails(app, team) : {
|
||
id: app.id,
|
||
name: app.name || "未命名應用",
|
||
type: app.type || "未知類型",
|
||
description: app.description || "無描述",
|
||
icon: getIconComponent(app.icon) || Brain,
|
||
iconColor: app.icon_color || "from-blue-500 to-purple-500",
|
||
author: app.creator_name || "未知作者",
|
||
department: app.creator_department || "未知部門",
|
||
category: app.category || "未分類",
|
||
likes: app.likes_count || 0,
|
||
views: app.views_count || 0,
|
||
rating: Number(app.rating) || 0,
|
||
createdAt: app.created_at
|
||
}
|
||
|
||
// 如果沒有圖標,使用默認的 Brain 圖標
|
||
const IconComponent = appData.icon || Brain
|
||
const likes = appData.likes || 0
|
||
const views = appData.views || 0
|
||
const rating = appData.rating || 0
|
||
|
||
return (
|
||
<Card
|
||
key={appData.id}
|
||
className="hover:shadow-md transition-all duration-200 cursor-pointer group"
|
||
onClick={() => handleAppClick(appData.id)}
|
||
>
|
||
<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">
|
||
{appData.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(appData.type)} text-xs mt-1`}>
|
||
{appData.type}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{appData.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>{Number(rating).toFixed(1)}</span>
|
||
</div>
|
||
</div>
|
||
<div onClick={(e) => e.stopPropagation()}>
|
||
<LikeButton
|
||
appId={appData.id}
|
||
size="sm"
|
||
likeCount={likes}
|
||
showCount={true}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* App Detail Dialog */}
|
||
{selectedApp && <AppDetailDialog open={appDetailOpen} onOpenChange={setAppDetailOpen} app={selectedApp} />}
|
||
</>
|
||
)
|
||
}
|