Files
ai-showcase-platform/components/favorites-page.tsx

351 lines
14 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 } from "react"
import { useAuth } from "@/contexts/auth-context"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Heart, ExternalLink, Star, Eye, ThumbsUp, MessageSquare, Brain, ImageIcon, Mic, MessageSquare as MessageSquareIcon, Settings, Zap, TrendingUp, Target, Users, Lightbulb, Search, Database, BarChart3, Camera, Smartphone, Monitor, Globe, FileText, Bot } from "lucide-react"
export function FavoritesPage() {
const { user } = useAuth()
const [sortBy, setSortBy] = useState("favorited")
const [filterDepartment, setFilterDepartment] = useState("all")
const [favoriteApps, setFavoriteApps] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(false)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
// 載入收藏列表
const loadFavorites = async (page: number = 1) => {
if (!user) return
setIsLoading(true)
try {
const response = await fetch(`/api/user/favorites?userId=${user.id}&page=${page}&limit=12`)
const data = await response.json()
if (data.success) {
setFavoriteApps(data.data.apps)
setTotalPages(data.data.pagination.totalPages)
setCurrentPage(page)
}
} catch (error) {
console.error('載入收藏列表錯誤:', error)
} finally {
setIsLoading(false)
}
}
// 初始載入
useEffect(() => {
if (user) {
loadFavorites()
}
}, [user])
const handleUseApp = async (app: any) => {
try {
// Increment view count when using the app
if (user) {
const response = await fetch(`/api/apps/${app.id}/interactions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'view',
userId: user.id
})
})
if (response.ok) {
// 記錄用戶活動
try {
const activityResponse = 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,
details: {
appName: app.name,
timestamp: new Date().toISOString()
}
})
})
if (activityResponse.ok) {
} else {
console.error('活動記錄失敗:', activityResponse.status, activityResponse.statusText)
}
} catch (error) {
console.error('記錄活動失敗:', error)
}
// Reload favorites to update view count
await loadFavorites(currentPage)
} else {
console.error('增加查看次數失敗:', response.status, response.statusText)
}
} else {
}
} catch (error) {
console.error('增加查看次數失敗:', error)
}
// Open app in new tab
if (app.appUrl) {
const url = app.appUrl.startsWith('http') ? app.appUrl : `https://${app.appUrl}`
window.open(url, "_blank", "noopener,noreferrer")
} else {
}
}
// 圖標映射函數
const getIconComponent = (iconName: string) => {
const iconMap: { [key: string]: any } = {
'Bot': Bot,
'Brain': Brain,
'ImageIcon': ImageIcon,
'Mic': Mic,
'MessageSquare': MessageSquareIcon,
'Settings': Settings,
'Zap': Zap,
'TrendingUp': TrendingUp,
'Star': Star,
'Heart': Heart,
'Eye': Eye,
'Target': Target,
'Users': Users,
'Lightbulb': Lightbulb,
'Search': Search,
'Database': Database,
'BarChart3': BarChart3,
'Camera': Camera,
'Smartphone': Smartphone,
'Monitor': Monitor,
'Globe': Globe,
'FileText': FileText,
}
return iconMap[iconName] || Bot
}
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",
: "bg-pink-100 text-pink-800 border-pink-200",
: "bg-indigo-100 text-indigo-800 border-indigo-200",
: "bg-cyan-100 text-cyan-800 border-cyan-200",
: "bg-teal-100 text-teal-800 border-teal-200",
: "bg-yellow-100 text-yellow-800 border-yellow-200",
: "bg-rose-100 text-rose-800 border-rose-200",
: "bg-emerald-100 text-emerald-800 border-emerald-200",
: "bg-violet-100 text-violet-800 border-violet-200",
}
return colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800 border-gray-200"
}
const filteredAndSortedApps = favoriteApps
.filter((app) => filterDepartment === "all" || app.department === filterDepartment)
.sort((a, b) => {
switch (sortBy) {
case "name":
return a.name.localeCompare(b.name)
case "creator":
return a.creator.localeCompare(b.creator)
case "department":
return a.department.localeCompare(b.department)
case "favorited":
return new Date(b.favoritedAt).getTime() - new Date(a.favoritedAt).getTime()
default:
return 0
}
})
return (
<div className="space-y-6">
{/* Stats */}
<div className="flex items-center justify-end">
<div className="text-sm text-gray-500">
{favoriteApps.length}
</div>
</div>
{/* Filter and Sort Controls */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div className="flex flex-col sm:flex-row gap-4">
<Select value={filterDepartment} onValueChange={setFilterDepartment}>
<SelectTrigger className="w-48">
<SelectValue placeholder="選擇部門" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="ACBU">ACBU</SelectItem>
<SelectItem value="AUBU">AUBU</SelectItem>
<SelectItem value="FAB3">FAB3</SelectItem>
<SelectItem value="FNBU">FNBU</SelectItem>
<SelectItem value="HQBU">HQBU</SelectItem>
<SelectItem value="HRBU">HRBU</SelectItem>
<SelectItem value="IBU">IBU</SelectItem>
<SelectItem value="ITBU">ITBU</SelectItem>
<SelectItem value="MBU1">MBU1</SelectItem>
<SelectItem value="PBU">PBU</SelectItem>
<SelectItem value="SBG">SBG</SelectItem>
<SelectItem value="SBU">SBU</SelectItem>
<SelectItem value="法務室"></SelectItem>
<SelectItem value="關係企業發展"></SelectItem>
<SelectItem value="稽核室"></SelectItem>
<SelectItem value="總經理室"></SelectItem>
<SelectItem value="HR">HR</SelectItem>
<SelectItem value="Finance">Finance</SelectItem>
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-48">
<SelectValue placeholder="排序方式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="name"></SelectItem>
<SelectItem value="creator"></SelectItem>
<SelectItem value="department"></SelectItem>
<SelectItem value="favorited"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Loading State */}
{isLoading && (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500 mx-auto mb-2"></div>
<p className="text-gray-500">...</p>
</div>
)}
{/* Apps Grid */}
{!isLoading && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredAndSortedApps.map((app) => {
const IconComponent = getIconComponent(app.icon || 'Heart')
return (
<Card key={app.id} className="group hover:shadow-lg transition-all duration-300">
<CardContent className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<div className={`w-12 h-12 bg-gradient-to-r ${app.iconColor || 'from-purple-500 to-pink-500'} rounded-lg flex items-center justify-center`}>
<IconComponent className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-semibold text-lg group-hover:text-purple-600 transition-colors">
{app.name}
</h3>
<p className="text-sm text-gray-500">by {app.creator}</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleUseApp(app)}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
<ExternalLink className="w-4 h-4" />
</Button>
</div>
<p className="text-gray-600 mb-4 line-clamp-2">{app.description}</p>
<div className="flex flex-wrap gap-2 mb-4">
<Badge variant="secondary" className={getTypeColor(app.type)}>
{app.type}
</Badge>
<Badge variant="outline">{app.department}</Badge>
</div>
{/* 統計數據區塊 - 優化佈局 */}
<div className="space-y-4">
{/* 評分 - 突出顯示 */}
<div className="flex items-center justify-center bg-gradient-to-r from-yellow-50 to-orange-50 rounded-lg px-4 py-3 border border-yellow-200">
<Star className="w-5 h-5 text-yellow-500 mr-2" />
<span className="text-lg font-bold text-gray-800">{Number(app.rating).toFixed(1)}</span>
<span className="text-sm text-gray-500 ml-1"></span>
</div>
{/* 其他統計數據 - 3列佈局 */}
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col items-center space-y-1 text-sm text-gray-600 bg-gray-50 rounded-lg px-2 py-2">
<Eye className="w-4 h-4 text-blue-500" />
<span className="font-medium text-base">{app.views}</span>
<span className="text-xs text-gray-400"></span>
</div>
<div className="flex flex-col items-center space-y-1 text-sm text-gray-600 bg-gray-50 rounded-lg px-2 py-2">
<ThumbsUp className="w-4 h-4 text-red-500" />
<span className="font-medium text-base">{app.likes}</span>
<span className="text-xs text-gray-400"></span>
</div>
<div className="flex flex-col items-center space-y-1 text-sm text-gray-600 bg-gray-50 rounded-lg px-2 py-2">
<MessageSquare className="w-4 h-4 text-green-500" />
<span className="font-medium text-base">{app.reviewCount}</span>
<span className="text-xs text-gray-400"></span>
</div>
</div>
{/* 使用應用按鈕 */}
<Button
onClick={() => handleUseApp(app)}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white py-2.5"
>
使
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
)}
{/* Empty State */}
{!isLoading && filteredAndSortedApps.length === 0 && (
<div className="text-center py-12">
<Heart className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-500 mb-2"></h3>
<p className="text-gray-400"> AI </p>
</div>
)}
{/* Pagination */}
{!isLoading && totalPages > 1 && (
<div className="flex justify-center space-x-2">
<Button
variant="outline"
onClick={() => loadFavorites(currentPage - 1)}
disabled={currentPage === 1}
>
</Button>
<span className="flex items-center px-4 text-sm text-gray-500">
{currentPage} {totalPages}
</span>
<Button
variant="outline"
onClick={() => loadFavorites(currentPage + 1)}
disabled={currentPage === totalPages}
>
</Button>
</div>
)}
</div>
)
}