實作個人收藏、個人活動紀錄
This commit is contained in:
@@ -1,28 +1,158 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
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 } from "lucide-react"
|
||||
|
||||
// Favorite apps data - empty for production
|
||||
const mockFavoriteApps: any[] = []
|
||||
import { Heart, ExternalLink, Star, Eye, ThumbsUp, MessageSquare, Brain, ImageIcon, Mic, MessageSquare as MessageSquareIcon, Settings, Zap, TrendingUp, Target, Users, Lightbulb, Search } from "lucide-react"
|
||||
|
||||
export function FavoritesPage() {
|
||||
const { user } = useAuth()
|
||||
const [sortBy, setSortBy] = useState("name")
|
||||
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 handleUseApp = (app: any) => {
|
||||
// Open app in new tab
|
||||
window.open(app.url, "_blank")
|
||||
console.log(`Opening app: ${app.name}`)
|
||||
// 載入收藏列表
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredAndSortedApps = mockFavoriteApps
|
||||
// 初始載入
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadFavorites()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const handleUseApp = async (app: any) => {
|
||||
console.log('handleUseApp 被調用', { user: user?.id, appId: app.id, appName: app.name })
|
||||
|
||||
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 {
|
||||
console.log('開始記錄用戶活動...')
|
||||
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) {
|
||||
console.log('活動記錄成功')
|
||||
} 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 {
|
||||
console.log('用戶未登入,跳過活動記錄')
|
||||
}
|
||||
} 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 {
|
||||
console.log(`Opening app: ${app.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 圖標映射函數
|
||||
const getIconComponent = (iconName: string) => {
|
||||
const iconMap: { [key: string]: any } = {
|
||||
'Bot': 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,
|
||||
}
|
||||
return iconMap[iconName] || Heart
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -32,6 +162,8 @@ export function FavoritesPage() {
|
||||
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
|
||||
}
|
||||
@@ -39,91 +171,163 @@ export function FavoritesPage() {
|
||||
|
||||
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 gap-3">
|
||||
<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="ITBU">ITBU</SelectItem>
|
||||
<SelectItem value="HR">HR</SelectItem>
|
||||
<SelectItem value="Finance">Finance</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="排序方式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">名稱</SelectItem>
|
||||
<SelectItem value="creator">開發者</SelectItem>
|
||||
<SelectItem value="department">部門</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={filterDepartment} onValueChange={setFilterDepartment}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="部門篩選" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部部門</SelectItem>
|
||||
<SelectItem value="HQBU">HQBU</SelectItem>
|
||||
<SelectItem value="ITBU">ITBU</SelectItem>
|
||||
<SelectItem value="MBU1">MBU1</SelectItem>
|
||||
<SelectItem value="SBU">SBU</SelectItem>
|
||||
<SelectItem value="name">按名稱排序</SelectItem>
|
||||
<SelectItem value="creator">按創作者排序</SelectItem>
|
||||
<SelectItem value="department">按部門排序</SelectItem>
|
||||
<SelectItem value="favorited">按收藏時間排序</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500">共 {filteredAndSortedApps.length} 個收藏應用</div>
|
||||
</div>
|
||||
|
||||
{/* Favorites Grid */}
|
||||
{filteredAndSortedApps.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredAndSortedApps.map((app) => (
|
||||
<Card key={app.id} className="h-full flex flex-col hover:shadow-lg transition-shadow">
|
||||
<CardContent className="p-6 flex flex-col h-full">
|
||||
{/* Header with heart icon */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex-1 pr-2">{app.name}</h3>
|
||||
<Heart className="w-5 h-5 text-red-500 fill-current flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2 flex-grow">{app.description}</p>
|
||||
|
||||
{/* Developer and Department */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-gray-700">開發者: {app.creator}</span>
|
||||
<Badge variant="outline" className="bg-gray-100 text-gray-700 border-gray-200">
|
||||
{app.department}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-6 flex-grow">
|
||||
{app.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="mt-auto flex-shrink-0">
|
||||
<Button className="w-full bg-black hover:bg-gray-800 text-white" onClick={() => handleUseApp(app)}>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
使用應用
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{/* 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-xl font-semibold text-gray-600 mb-2">暫無收藏應用</h3>
|
||||
<p className="text-gray-500">
|
||||
{filterDepartment !== "all"
|
||||
? "該部門暫無收藏的應用,請嘗試其他篩選條件"
|
||||
: "您還沒有收藏任何應用,快去探索並收藏您喜歡的 AI 應用吧!"}
|
||||
</p>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user