Files
ai-showcase-platform/components/app-detail-dialog.tsx

925 lines
39 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, useEffect, useCallback } 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,
ThumbsUp,
Info,
MessageSquare,
User,
Calendar,
Building,
TrendingUp,
Users,
BarChart3,
Brain,
ImageIcon,
Mic,
Settings,
Zap,
Target,
Bookmark,
Lightbulb,
Search,
Plus,
X,
ChevronLeft,
ChevronRight,
ArrowLeft,
Trophy,
Award,
Medal,
Bot,
Code,
Database,
Palette,
Volume2,
Camera,
Smartphone,
Monitor,
Globe,
FileText,
} from "lucide-react"
import { FavoriteButton } from "./favorite-button"
import { ReviewSystem } from "./reviews/review-system"
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"
interface AppDetailDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
app: {
id: string
name: string
type: string
department: string
description: string
icon: any
iconColor?: string
creator: string
featured: boolean
judgeScore: number
likesCount?: number
viewsCount?: number
rating?: number
reviewsCount?: number
createdAt?: string
}
}
// 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,
favorites: 0,
rating: 0,
reviewCount: 0
},
usage: {
dailyUsers: 0,
weeklyUsers: 0,
monthlyUsers: 0,
totalSessions: 0,
topDepartments: [],
trendData: []
}
})
const [isLoadingStats, setIsLoadingStats] = useState(false)
const [selectedDepartment, setSelectedDepartment] = useState<string | null>(null)
// 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 COLORS = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#84cc16', '#f97316']
// 圖標映射函數
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,
'Star': Star,
'Heart': Heart,
'Eye': Eye,
'Trophy': Trophy,
'Award': Award,
'Medal': Medal,
'Target': Target,
'Users': Users,
'Lightbulb': Lightbulb,
'Plus': Plus,
'X': X,
'ChevronLeft': ChevronLeft,
'ChevronRight': ChevronRight,
'ArrowLeft': ArrowLeft,
'Settings': Settings,
'Camera': Camera,
'Smartphone': Smartphone,
'Monitor': Monitor,
'Globe': Globe,
'FileText': FileText,
}
return iconMap[iconName] || Bot // 預設使用 Bot 圖標
}
const IconComponent = getIconComponent(app.icon || 'Bot')
// 優先使用從 API 載入的實際數據,如果沒有則使用 app 對象的數據
const likes = appStats.basic?.likes || (app as any).likesCount || 0
const views = appStats.basic?.views || (app as any).viewsCount || 0
const rating = appStats.basic?.rating || (app as any).rating || 0
const reviewsCount = appStats.basic?.reviewCount || (app as any).reviewsCount || 0
const favorites = appStats.basic?.favorites || 0
// 使用從 API 載入的實際數據
const usageStats = appStats.usage
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 loadAppStats = useCallback(async (customStartDate?: string, customEndDate?: string, department?: string) => {
if (!app.id) return
setIsLoadingStats(true)
try {
// 構建查詢參數
const params = new URLSearchParams()
if (customStartDate) params.append('startDate', customStartDate)
if (customEndDate) params.append('endDate', customEndDate)
if (department) params.append('department', department)
const url = `/api/apps/${app.id}/stats${params.toString() ? `?${params.toString()}` : ''}`
const response = await fetch(url)
if (!response.ok) {
console.error('❌ API 響應錯誤:', response.status, response.statusText)
return
}
const data = await response.json()
if (data.success) {
setAppStats(data.data)
setCurrentRating(data.data.basic.rating)
setReviewCount(data.data.basic.reviewCount)
} else {
console.error('❌ 應用統計數據加載失敗:', data)
}
} catch (error) {
console.error('載入應用統計數據錯誤:', error)
} finally {
setIsLoadingStats(false)
}
}, [app.id])
const handleRatingUpdate = useCallback(async (newRating: number, newReviewCount: number) => {
setCurrentRating(newRating)
setReviewCount(newReviewCount)
// Reload stats after rating update
await loadAppStats()
}, [loadAppStats])
// 處理日期範圍變更
const handleDateRangeChange = useCallback(async () => {
if (startDate && endDate) {
await loadAppStats(startDate, endDate, selectedDepartment || undefined)
}
}, [startDate, endDate, selectedDepartment, loadAppStats])
// 處理日期變更時重置部門選擇
const handleDateChange = useCallback((newStartDate: string, newEndDate: string) => {
setStartDate(newStartDate)
setEndDate(newEndDate)
setSelectedDepartment(null) // 重置部門選擇
}, [])
// 處理部門選擇
const handleDepartmentSelect = useCallback(async (department: string | null) => {
setSelectedDepartment(department)
if (startDate && endDate) {
await loadAppStats(startDate, endDate, department || undefined)
}
}, [startDate, endDate, loadAppStats])
// 當對話框打開時載入統計數據
useEffect(() => {
if (open && app.id) {
loadAppStats()
}
}, [open, app.id, loadAppStats])
// 當日期範圍變更時重新載入使用趨勢數據
useEffect(() => {
if (open && app.id && startDate && endDate) {
handleDateRangeChange()
}
}, [startDate, endDate, open, app.id, handleDateRangeChange])
const handleTryApp = async () => {
if (user) {
addToRecentApps(app.id.toString())
// 記錄用戶活動
try {
const response = await fetch('/api/user/activity', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: user.id,
action: 'view',
resourceType: 'app',
resourceId: app.id.toString(),
details: {
appName: app.name,
timestamp: new Date().toISOString()
}
})
})
if (response.ok) {
} else {
console.error('活動記錄失敗:', response.status, response.statusText)
}
} catch (error) {
console.error('記錄活動失敗:', error)
}
} else {
}
// Increment view count when trying the app
await incrementViewCount(app.id.toString())
// Reload stats after incrementing view count
await loadAppStats()
// Get app URL from database or fallback to default URLs
const appUrl = (app as any).appUrl || (app as any).app_url
if (appUrl) {
// Ensure URL has protocol
const url = appUrl.startsWith('http') ? appUrl : `https://${appUrl}`
window.open(url, "_blank", "noopener,noreferrer")
} else {
// Fallback to default URLs for testing
const defaultUrls: 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 fallbackUrl = defaultUrls[app.id.toString()]
if (fallbackUrl) {
window.open(fallbackUrl, "_blank", "noopener,noreferrer")
} else {
console.warn('No app URL found for app:', app.id)
// Show a toast or alert to user
alert('此應用暫無可用連結')
}
}
}
// 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 ${(app as any).iconColor || '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 justify-between">
<div className="flex items-center space-x-6 text-sm text-gray-600">
<div className="flex items-center space-x-1">
<ThumbsUp 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>{Number(rating).toFixed(1)}</span>
{reviewsCount > 0 && <span className="text-gray-500">({reviewsCount} )</span>}
</div>
</div>
<div className="flex items-center space-x-3">
<FavoriteButton appId={app.id.toString()} size="md" />
<Button
onClick={handleTryApp}
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-6 py-2"
>
</Button>
</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">
{(app as any).createdAt ? new Date((app as any).createdAt).toLocaleDateString('zh-TW', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) : '未知'}
</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> {app.type} </li>
<li> {app.department} </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>
</TabsContent>
<TabsContent value="statistics" className="space-y-6">
{/* 基本統計數據 */}
<div className="grid grid-cols-1 md:grid-cols-5 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 ? '...' : 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>
<ThumbsUp className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{isLoadingStats ? '...' : 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>
<Heart className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">
{isLoadingStats ? '...' : favorites}
</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 ? '...' : Number(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 ? '...' : reviewsCount}
</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>
{/* Date Range Filter */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>使使</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row gap-3 items-end">
<div className="flex flex-col sm:flex-row gap-3 flex-1">
<div className="flex items-center space-x-2">
<Label htmlFor="start-date" className="text-sm whitespace-nowrap min-w-[60px]">
</Label>
<Input
id="start-date"
type="date"
value={startDate}
onChange={(e) => handleDateChange(e.target.value, endDate)}
className="w-36"
max={endDate}
/>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="end-date" className="text-sm whitespace-nowrap min-w-[60px]">
</Label>
<Input
id="end-date"
type="date"
value={endDate}
onChange={(e) => handleDateChange(startDate, e.target.value)}
className="w-36"
min={startDate}
max={new Date().toISOString().split("T")[0]}
/>
</div>
</div>
<Button
onClick={handleDateRangeChange}
disabled={isLoadingStats || !startDate || !endDate}
size="sm"
variant="outline"
className="whitespace-nowrap"
>
{isLoadingStats ? (
<>
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-500 mr-1"></div>
</>
) : (
'重新載入'
)}
</Button>
</div>
</CardContent>
</Card>
{/* Analytics Layout */}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* Department Usage Pie Chart */}
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
<CardDescription>使</CardDescription>
</CardHeader>
<CardContent>
{isLoadingStats ? (
<div className="h-80 flex items-center justify-center bg-gray-50 rounded-lg">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
<p className="text-gray-500">...</p>
</div>
</div>
) : usageStats.topDepartments && usageStats.topDepartments.length > 0 ? (
<div className="space-y-4">
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={usageStats.topDepartments.map((dept: any, index: number) => ({
name: dept.department || '未知部門',
value: dept.count,
color: COLORS[index % COLORS.length]
}))}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${((percent || 0) * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
onClick={(data) => {
const department = data.name === '未知部門' ? null : data.name
handleDepartmentSelect(department)
}}
className="cursor-pointer"
>
{usageStats.topDepartments.map((dept: any, index: number) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
className="cursor-pointer hover:opacity-80 transition-opacity"
/>
))}
</Pie>
<Tooltip formatter={(value: any) => [`${value}`, '使用人數']} />
</PieChart>
</ResponsiveContainer>
{/* Department Legend */}
<div className="space-y-2">
{usageStats.topDepartments.map((dept: any, index: number) => {
const totalUsers = usageStats.topDepartments.reduce((sum: number, d: any) => sum + d.count, 0)
const percentage = totalUsers > 0 ? Math.round((dept.count / totalUsers) * 100) : 0
const isSelected = selectedDepartment === dept.department
return (
<div
key={dept.department || index}
className={`flex items-center justify-between p-2 rounded-lg cursor-pointer transition-colors ${
isSelected ? 'bg-blue-50 border border-blue-200' : 'hover:bg-gray-50'
}`}
onClick={() => {
const department = dept.department === '未知部門' ? null : dept.department
handleDepartmentSelect(department)
}}
>
<div className="flex items-center space-x-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: COLORS[index % COLORS.length] }}
/>
<span className="font-medium">{dept.department || '未知部門'}</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">{dept.count} </span>
<span className="text-sm font-medium">{percentage}%</span>
</div>
</div>
)
})}
</div>
</div>
) : (
<div className="h-80 flex items-center justify-center bg-gray-50 rounded-lg">
<div className="text-center text-gray-500">
<Building className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p>使</p>
</div>
</div>
)}
</CardContent>
</Card>
{/* Usage Trends */}
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
<CardDescription>
{selectedDepartment ? `${selectedDepartment} 部門的使用趨勢` : '查看指定時間範圍內的使用者活躍度'}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{isLoadingStats ? (
<div className="h-80 flex items-center justify-center bg-gray-50 rounded-lg">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
<p className="text-gray-500">使...</p>
</div>
</div>
) : usageStats.trendData && usageStats.trendData.length > 0 ? (
<>
{/* 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(400, usageStats.trendData.length * 80)}px`, // Dynamic width based on data points
}}
>
{/* Month/Year Section Headers - Full Width */}
<div className="absolute top-2 left-4 right-4 flex">
{(() => {
const sections = getDateSections(usageStats.trendData)
const totalBars = usageStats.trendData.length
const barWidth = 60 // 每個柱子寬度
const barGap = 12 // 柱子間距
const chartLeft = 20 // paddingLeft
const totalChartWidth = totalBars * barWidth + (totalBars - 1) * barGap
return Object.entries(sections).map(([key, section]) => {
// 計算該月份在柱狀圖中的實際位置
const sectionStartBar = section.startIndex
const sectionEndBar = section.endIndex
const sectionBarCount = sectionEndBar - sectionStartBar + 1
// 計算該月份標籤的起始位置(相對於圖表區域)
const sectionLeft = sectionStartBar * (barWidth + barGap)
const sectionWidth = sectionBarCount * barWidth + (sectionBarCount - 1) * barGap
// 轉換為相對於整個容器的百分比(從左邊界開始)
const containerWidth = chartLeft + totalChartWidth
const leftPercent = ((sectionLeft + chartLeft) / containerWidth) * 100
const widthPercent = (sectionWidth / containerWidth) * 100
return (
<div
key={key}
className="absolute text-xs font-medium text-gray-700 bg-blue-50 px-3 py-1 rounded shadow-sm border border-blue-200"
style={{
left: `${leftPercent}%`,
width: `${widthPercent}%`,
textAlign: "center",
minWidth: "60px", // 確保標籤有最小寬度
}}
>
{section.label}
</div>
)
})
})()}
</div>
{/* Y-axis labels and grid lines */}
<div className="absolute left-2 top-12 bottom-8 flex flex-col justify-between text-xs text-gray-500">
<span>{Math.max(...usageStats.trendData.map((d: any) => d.users))}</span>
<span>{Math.round(Math.max(...usageStats.trendData.map((d: any) => d.users)) * 0.75)}</span>
<span>{Math.round(Math.max(...usageStats.trendData.map((d: any) => d.users)) * 0.5)}</span>
<span>{Math.round(Math.max(...usageStats.trendData.map((d: any) => d.users)) * 0.25)}</span>
<span>0</span>
</div>
{/* Grid lines */}
<div className="absolute left-10 top-12 bottom-8 right-4 flex flex-col justify-between">
{[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => (
<div key={index} className="w-full h-px bg-gray-200 opacity-50"></div>
))}
</div>
{/* Chart Bars */}
<div className="h-full flex items-end justify-start gap-3" style={{ paddingTop: "40px", paddingLeft: "40px" }}>
{usageStats.trendData.map((day: any, index: number) => {
const maxUsers = Math.max(...usageStats.trendData.map((d: any) => d.users))
const minUsers = Math.min(...usageStats.trendData.map((d: any) => 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] as any).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 flex-col items-center group relative"
style={{ width: "80px" }}
>
{/* 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-8 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 shadow-sm"
style={{ height: `${normalizedHeight}%` }}
>
{/* Value label */}
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs font-medium text-gray-700 bg-white/90 px-2 py-1 rounded shadow-sm border">
{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 && usageStats.trendData.length > 20 && (
<div className="text-xs text-gray-500 text-center">💡 </div>
)}
</>
) : (
<div className="h-80 flex items-center justify-center bg-gray-50 rounded-lg">
<div className="text-center text-gray-500">
<TrendingUp className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p>使</p>
<p className="text-sm mt-1"></p>
</div>
</div>
)}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="reviews" className="space-y-6">
<ReviewSystem
appId={app.id.toString()}
appName={app.name}
currentRating={currentRating}
onRatingUpdate={handleRatingUpdate}
/>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
)
}