Files
ai-showcase-platform/components/app-detail-dialog.tsx
2025-09-09 18:18:02 +08:00

573 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
import { useAuth } from "@/contexts/auth-context"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Star,
Eye,
Heart,
Info,
MessageSquare,
User,
Calendar,
Building,
TrendingUp,
Users,
BarChart3,
} from "lucide-react"
import { FavoriteButton } from "./favorite-button"
import { ReviewSystem } from "./reviews/review-system"
interface AppDetailDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
app: {
id: number
name: string
type: string
department: string
description: string
icon: any
creator: string
featured: boolean
judgeScore: number
}
}
// Usage statistics data - empty for production
const getAppUsageStats = (appId: string, startDate: string, endDate: string) => {
return {
dailyUsers: 0,
weeklyUsers: 0,
monthlyUsers: 0,
totalSessions: 0,
topDepartments: [],
trendData: [],
}
}
export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProps) {
const { user, addToRecentApps, getAppLikes, incrementViewCount, getViewCount, getAppRating } = useAuth()
const [currentRating, setCurrentRating] = useState(getAppRating(app.id.toString()))
const [reviewCount, setReviewCount] = useState(0)
const [appStats, setAppStats] = useState({
basic: {
views: 0,
likes: 0,
rating: 0,
reviewCount: 0
},
usage: {
dailyUsers: 0,
weeklyUsers: 0,
monthlyUsers: 0,
totalSessions: 0,
topDepartments: [],
trendData: []
}
})
const [isLoadingStats, setIsLoadingStats] = useState(false)
// Date range for usage trends
const [startDate, setStartDate] = useState(() => {
const date = new Date()
date.setDate(date.getDate() - 6) // Default to last 7 days
return date.toISOString().split("T")[0]
})
const [endDate, setEndDate] = useState(() => {
return new Date().toISOString().split("T")[0]
})
const IconComponent = app.icon
const likes = getAppLikes(app.id.toString())
const views = getViewCount(app.id.toString())
const usageStats = getAppUsageStats(app.id.toString(), startDate, endDate)
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 handleRatingUpdate = (newRating: number, newReviewCount: number) => {
setCurrentRating(newRating)
setReviewCount(newReviewCount)
}
// 載入應用統計數據
const loadAppStats = async () => {
if (!app.id) return
setIsLoadingStats(true)
try {
const response = await fetch(`/api/apps/${app.id}/stats`)
const data = await response.json()
if (data.success) {
setAppStats(data.data)
setCurrentRating(data.data.basic.rating)
setReviewCount(data.data.basic.reviewCount)
}
} catch (error) {
console.error('載入應用統計數據錯誤:', error)
} finally {
setIsLoadingStats(false)
}
}
// 當對話框打開時載入統計數據
React.useEffect(() => {
if (open && app.id) {
loadAppStats()
}
}, [open, app.id])
const handleTryApp = () => {
if (user) {
addToRecentApps(app.id.toString())
}
// Increment view count when trying the app
incrementViewCount(app.id.toString())
// Open external app URL in new tab
const appUrls: Record<string, string> = {
"1": "https://dify.example.com/chat-assistant",
"2": "https://image-gen.example.com",
"3": "https://speech.example.com",
"4": "https://recommend.example.com",
"5": "https://text-analysis.example.com",
"6": "https://ai-writing.example.com",
}
const appUrl = appUrls[app.id.toString()]
if (appUrl) {
window.open(appUrl, "_blank", "noopener,noreferrer")
}
}
// Helper function to group data by month/year for section headers
const getDateSections = (trendData: any[]) => {
const sections: { [key: string]: { startIndex: number; endIndex: number; label: string } } = {}
trendData.forEach((day, index) => {
const date = new Date(day.date)
const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
const label = date.toLocaleDateString("zh-TW", { year: "numeric", month: "long" })
if (!sections[yearMonth]) {
sections[yearMonth] = { startIndex: index, endIndex: index, label }
} else {
sections[yearMonth].endIndex = index
}
})
return sections
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-start 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">
<DialogTitle className="text-2xl font-bold mb-2">{app.name}</DialogTitle>
<DialogDescription className="text-base mb-3">{app.description}</DialogDescription>
<div className="flex flex-wrap gap-2 mb-4">
<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>
{app.featured && (
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800 border-yellow-200">
<Star className="w-3 h-3 mr-1" />
</Badge>
)}
</div>
<div className="flex items-center space-x-6 text-sm text-gray-600">
<div className="flex items-center space-x-1">
<Heart className="w-4 h-4" />
<span>{likes} </span>
</div>
<div className="flex items-center space-x-1">
<Eye className="w-4 h-4" />
<span>{views} </span>
</div>
<div className="flex items-center space-x-1">
<Star className="w-4 h-4 text-yellow-500" />
<span>{currentRating.toFixed(1)}</span>
{reviewCount > 0 && <span className="text-gray-500">({reviewCount} )</span>}
</div>
</div>
</div>
</div>
</DialogHeader>
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview" className="flex items-center space-x-2">
<Info className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="statistics" className="flex items-center space-x-2">
<BarChart3 className="w-4 h-4" />
<span>使</span>
</TabsTrigger>
<TabsTrigger value="reviews" className="flex items-center space-x-2">
<MessageSquare className="w-4 h-4" />
<span></span>
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="flex items-center space-x-3">
<User className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{app.creator}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Building className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">{app.department}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Calendar className="w-5 h-5 text-gray-500" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="font-medium">2024115</p>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-500 mb-2"></p>
<ul className="space-y-1 text-sm">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
<div className="pt-4 border-t">
<p className="text-sm text-gray-500 mb-2"></p>
<p className="text-gray-700 leading-relaxed">
{app.description}
使
</p>
</div>
</CardContent>
</Card>
<div className="flex items-center justify-between">
<FavoriteButton appId={app.id.toString()} size="default" showText={true} className="px-6" />
<Button
onClick={handleTryApp}
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
</Button>
</div>
</TabsContent>
<TabsContent value="statistics" className="space-y-6">
{/* 基本統計數據 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Eye className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">
{isLoadingStats ? '...' : appStats.basic.views}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Heart className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{isLoadingStats ? '...' : appStats.basic.likes}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Star className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-600">
{isLoadingStats ? '...' : appStats.basic.rating.toFixed(1)}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<MessageSquare className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{isLoadingStats ? '...' : appStats.basic.reviewCount}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</div>
{/* 使用趨勢 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<Users className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isLoadingStats ? '...' : appStats.usage.dailyUsers}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<TrendingUp className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isLoadingStats ? '...' : appStats.usage.weeklyUsers}
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">使</CardTitle>
<BarChart3 className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isLoadingStats ? '...' : appStats.usage.totalSessions.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground">使</p>
</CardContent>
</Card>
</div>
{/* Usage Trends with Date Range */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>使</CardTitle>
<CardDescription>使</CardDescription>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<Label htmlFor="start-date" className="text-sm">
</Label>
<Input
id="start-date"
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-36"
/>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="end-date" className="text-sm">
</Label>
<Input
id="end-date"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-36"
/>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Chart Container with Horizontal Scroll */}
<div className="w-full overflow-x-auto">
<div
className="h-80 relative bg-gray-50 rounded-lg p-4"
style={{
minWidth: `${Math.max(800, usageStats.trendData.length * 40)}px`, // Dynamic width based on data points
}}
>
{/* Month/Year Section Headers */}
<div className="absolute top-2 left-4 right-4 flex">
{(() => {
const sections = getDateSections(usageStats.trendData)
const totalBars = usageStats.trendData.length
return Object.entries(sections).map(([key, section]) => {
const width = ((section.endIndex - section.startIndex + 1) / totalBars) * 100
const left = (section.startIndex / totalBars) * 100
return (
<div
key={key}
className="absolute text-xs font-medium text-gray-600 bg-white/90 px-2 py-1 rounded shadow-sm border"
style={{
left: `${left}%`,
width: `${width}%`,
textAlign: "center",
}}
>
{section.label}
</div>
)
})
})()}
</div>
{/* Chart Bars */}
<div className="h-full flex items-end justify-between space-x-2" style={{ paddingTop: "40px" }}>
{usageStats.trendData.map((day, index) => {
const maxUsers = Math.max(...usageStats.trendData.map((d) => d.users))
const minUsers = Math.min(...usageStats.trendData.map((d) => d.users))
const range = maxUsers - minUsers
const normalizedHeight = range > 0 ? ((day.users - minUsers) / range) * 70 + 15 : 40
const currentDate = new Date(day.date)
const prevDate = index > 0 ? new Date(usageStats.trendData[index - 1].date) : null
// Check if this is the start of a new month/year for divider
const isNewMonth =
!prevDate ||
currentDate.getMonth() !== prevDate.getMonth() ||
currentDate.getFullYear() !== prevDate.getFullYear()
return (
<div
key={day.date}
className="flex-1 flex flex-col items-center group relative"
style={{ minWidth: "32px" }}
>
{/* Month divider line */}
{isNewMonth && index > 0 && (
<div className="absolute left-0 top-0 bottom-8 w-px bg-gray-300 opacity-50" />
)}
<div
className="w-full flex flex-col items-center justify-end"
style={{ height: "200px" }}
>
<div
className="w-full bg-gradient-to-t from-blue-500 to-purple-500 rounded-t-md transition-all duration-300 hover:from-blue-600 hover:to-purple-600 cursor-pointer relative"
style={{ height: `${normalizedHeight}%` }}
>
{/* Value label */}
<div className="absolute -top-5 left-1/2 transform -translate-x-1/2 text-xs font-medium text-gray-600 bg-white/80 px-1 rounded">
{day.users}
</div>
</div>
</div>
{/* Consistent day-only labels */}
<div className="text-xs text-gray-500 mt-2 text-center">{currentDate.getDate()}</div>
</div>
)
})}
</div>
</div>
</div>
{/* Scroll Hint */}
{usageStats.trendData.length > 20 && (
<div className="text-xs text-gray-500 text-center">💡 </div>
)}
</div>
</CardContent>
</Card>
{/* Department Usage */}
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
<CardDescription>使</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{usageStats.topDepartments.map((dept) => (
<div key={dept.name} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full" />
<span className="font-medium">{dept.name}</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">{dept.users} </span>
<span className="text-sm font-medium">{dept.percentage}%</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="reviews" className="space-y-6">
<ReviewSystem
appId={app.id.toString()}
appName={app.name}
currentRating={currentRating}
onRatingUpdate={handleRatingUpdate}
/>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
)
}