From 9c5dceb00122444f43315ec17b5dfea5cfc9d0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B3=E4=BD=A9=E5=BA=AD?= Date: Thu, 11 Sep 2025 17:40:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=A6=E4=BD=9C=E5=80=8B=E4=BA=BA=E6=94=B6?= =?UTF-8?q?=E8=97=8F=E3=80=81=E5=80=8B=E4=BA=BA=E6=B4=BB=E5=8B=95=E7=B4=80?= =?UTF-8?q?=E9=8C=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/admin/apps/[id]/stats/route.ts | 4 + app/api/admin/apps/route.ts | 2 +- app/api/admin/users/list/route.ts | 30 +- app/api/apps/[id]/favorite/route.ts | 124 +++ app/api/apps/[id]/interactions/route.ts | 98 ++ app/api/apps/[id]/reviews/route.ts | 99 ++ app/api/apps/[id]/stats/route.ts | 13 +- app/api/reviews/[id]/votes/route.ts | 69 ++ app/api/user/activity/route.ts | 79 ++ app/api/user/favorites/route.ts | 44 + app/api/user/interactions/route.ts | 41 + app/page.tsx | 62 +- components/admin/app-management.tsx | 13 +- components/admin/user-management.tsx | 4 +- components/app-detail-dialog.tsx | 519 ++++++---- components/auth/activity-records-dialog.tsx | 265 ++++-- components/favorite-button.tsx | 31 +- components/favorites-page.tsx | 366 +++++-- components/like-button.tsx | 79 +- components/reviews/review-system-fixed.tsx | 496 ++++++++++ components/reviews/review-system.tsx | 381 +++++--- contexts/auth-context.tsx | 271 ++++-- database-schema-simple.sql | 22 +- database-schema.sql | 22 +- lib/models.ts | 6 + lib/services/database-service.ts | 997 +++++++++++++++++++- scripts/check-app-status.js | 67 ++ scripts/fix-favorites-duplicates.js | 92 ++ scripts/test-favorites.js | 86 ++ 29 files changed, 3781 insertions(+), 601 deletions(-) create mode 100644 app/api/apps/[id]/favorite/route.ts create mode 100644 app/api/apps/[id]/interactions/route.ts create mode 100644 app/api/apps/[id]/reviews/route.ts create mode 100644 app/api/reviews/[id]/votes/route.ts create mode 100644 app/api/user/activity/route.ts create mode 100644 app/api/user/favorites/route.ts create mode 100644 app/api/user/interactions/route.ts create mode 100644 components/reviews/review-system-fixed.tsx create mode 100644 scripts/check-app-status.js create mode 100644 scripts/fix-favorites-duplicates.js create mode 100644 scripts/test-favorites.js diff --git a/app/api/admin/apps/[id]/stats/route.ts b/app/api/admin/apps/[id]/stats/route.ts index 565dd1f..7dae865 100644 --- a/app/api/admin/apps/[id]/stats/route.ts +++ b/app/api/admin/apps/[id]/stats/route.ts @@ -22,12 +22,16 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri // 獲取使用統計 const usageStats = await appService.getAppUsageStats(appId) + // 獲取收藏數量 + const favoritesCount = await appService.getAppFavoritesCount(appId) + return NextResponse.json({ success: true, data: { basic: { views: app.views_count || 0, likes: app.likes_count || 0, + favorites: favoritesCount, rating: ratingStats.averageRating || 0, reviewCount: ratingStats.totalRatings || 0 }, diff --git a/app/api/admin/apps/route.ts b/app/api/admin/apps/route.ts index 9ba6264..1481953 100644 --- a/app/api/admin/apps/route.ts +++ b/app/api/admin/apps/route.ts @@ -24,7 +24,7 @@ export async function GET(request: NextRequest) { }) // 獲取應用統計 - const stats = await appService.getAppStats() + const stats = await appService.getDashboardStats() return NextResponse.json({ success: true, diff --git a/app/api/admin/users/list/route.ts b/app/api/admin/users/list/route.ts index 76f8952..7214bd2 100644 --- a/app/api/admin/users/list/route.ts +++ b/app/api/admin/users/list/route.ts @@ -5,26 +5,38 @@ const userService = new UserService() export async function GET(request: NextRequest) { try { - // 獲取所有啟用狀態的用戶 + // 獲取所有用戶(包括不同狀態) const sql = ` - SELECT id, name, email, department, role + SELECT id, name, email, department, role, status, created_at, last_login FROM users - WHERE status = 'active' ORDER BY name ASC `; const users = await userService.query(sql); - return NextResponse.json({ - success: true, - data: { - users: users.map(user => ({ + // 為每個用戶獲取統計數據 + const usersWithStats = await Promise.all( + users.map(async (user) => { + const stats = await userService.getUserAppAndReviewStats(user.id); + return { id: user.id, name: user.name, email: user.email, department: user.department, - role: user.role - })) + role: user.role, + status: user.status, + createdAt: user.created_at, + lastLogin: user.last_login, + appCount: stats.appCount, + reviewCount: stats.reviewCount + }; + }) + ); + + return NextResponse.json({ + success: true, + data: { + users: usersWithStats } }) diff --git a/app/api/apps/[id]/favorite/route.ts b/app/api/apps/[id]/favorite/route.ts new file mode 100644 index 0000000..26e1282 --- /dev/null +++ b/app/api/apps/[id]/favorite/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +// 添加收藏 +export async function POST(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + const body = await request.json() + const { userId } = body + + if (!userId) { + return NextResponse.json( + { success: false, error: '用戶ID不能為空' }, + { status: 400 } + ) + } + + // 檢查應用是否存在 + const app = await appService.getAppById(appId) + if (!app) { + return NextResponse.json( + { success: false, error: '應用不存在' }, + { status: 404 } + ) + } + + // 添加收藏 + const result = await appService.addFavorite(userId, appId) + + if (result.success) { + return NextResponse.json({ + success: true, + message: '收藏成功' + }) + } else { + // 如果是重複收藏,返回 409 狀態碼 + const statusCode = result.error === '已經收藏過此應用' ? 409 : 400 + return NextResponse.json( + { success: false, error: result.error }, + { status: statusCode } + ) + } + + } catch (error) { + console.error('添加收藏錯誤:', error) + return NextResponse.json( + { success: false, error: '添加收藏時發生錯誤' }, + { status: 500 } + ) + } +} + +// 移除收藏 +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json( + { success: false, error: '用戶ID不能為空' }, + { status: 400 } + ) + } + + // 移除收藏 + const result = await appService.removeFavorite(userId, appId) + + if (result.success) { + return NextResponse.json({ + success: true, + message: '取消收藏成功' + }) + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ) + } + + } catch (error) { + console.error('移除收藏錯誤:', error) + return NextResponse.json( + { success: false, error: '移除收藏時發生錯誤' }, + { status: 500 } + ) + } +} + +// 檢查收藏狀態 +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id: appId } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json( + { success: false, error: '用戶ID不能為空' }, + { status: 400 } + ) + } + + // 檢查收藏狀態 + const isFavorited = await appService.isFavorited(userId, appId) + + return NextResponse.json({ + success: true, + data: { + isFavorited + } + }) + + } catch (error) { + console.error('檢查收藏狀態錯誤:', error) + return NextResponse.json( + { success: false, error: '檢查收藏狀態時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/apps/[id]/interactions/route.ts b/app/api/apps/[id]/interactions/route.ts new file mode 100644 index 0000000..880424f --- /dev/null +++ b/app/api/apps/[id]/interactions/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +// 獲取應用的統計數據 +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: appId } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + // 獲取應用的統計數據 + const stats = await appService.getAppStats(appId, userId || undefined) + + return NextResponse.json({ + success: true, + data: { + likesCount: stats.likes_count || 0, + viewsCount: stats.views_count || 0, + rating: stats.average_rating || 0, + reviewsCount: stats.reviews_count || 0, + userLiked: stats.userLiked || false + } + }) + } catch (error) { + console.error('獲取應用統計數據錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取應用統計數據時發生錯誤' }, + { status: 500 } + ) + } +} + +// 更新應用統計數據(按讚、觀看等) +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: appId } = await params + const { action, userId } = await request.json() + + if (!userId) { + return NextResponse.json( + { success: false, error: '需要用戶身份驗證' }, + { status: 401 } + ) + } + + let result = false + + switch (action) { + case 'like': + result = await appService.toggleAppLike(appId, userId) + break + case 'view': + result = await appService.incrementAppViews(appId, userId) + break + case 'favorite': + result = await appService.toggleAppFavorite(appId, userId) + break + default: + return NextResponse.json( + { success: false, error: '無效的操作類型' }, + { status: 400 } + ) + } + + if (result) { + // 重新獲取更新後的統計數據 + const stats = await appService.getAppStats(appId) + return NextResponse.json({ + success: true, + data: { + likesCount: stats.likes_count || 0, + viewsCount: stats.views_count || 0, + rating: stats.average_rating || 0, + reviewsCount: stats.reviews_count || 0 + } + }) + } else { + return NextResponse.json( + { success: false, error: '操作失敗' }, + { status: 500 } + ) + } + } catch (error) { + console.error('更新應用統計數據錯誤:', error) + return NextResponse.json( + { success: false, error: '更新應用統計數據時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/apps/[id]/reviews/route.ts b/app/api/apps/[id]/reviews/route.ts new file mode 100644 index 0000000..5a3db17 --- /dev/null +++ b/app/api/apps/[id]/reviews/route.ts @@ -0,0 +1,99 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +// 獲取應用的評論列表 +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: appId } = await params + const { searchParams } = new URL(request.url) + const limit = parseInt(searchParams.get('limit') || '5') // 預設5筆 + const offset = parseInt(searchParams.get('offset') || '0') + const page = parseInt(searchParams.get('page') || '1') + + const reviews = await appService.getAppReviews(appId, limit, offset) + const totalReviews = await appService.getAppReviewCount(appId) + const totalPages = Math.ceil(totalReviews / limit) + + return NextResponse.json({ + success: true, + data: { + reviews, + pagination: { + page, + limit, + total: totalReviews, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + } + } + }) + } catch (error) { + console.error('獲取應用評論錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取應用評論時發生錯誤' }, + { status: 500 } + ) + } +} + +// 創建新的評論 +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: appId } = await params + const { userId, rating, comment, reviewId } = await request.json() + + if (!userId || !rating || !comment) { + return NextResponse.json( + { success: false, error: '缺少必要參數' }, + { status: 400 } + ) + } + + // 檢查評分範圍 + if (rating < 1 || rating > 5) { + return NextResponse.json( + { success: false, error: '評分必須在 1-5 之間' }, + { status: 400 } + ) + } + + let resultReviewId: string | null = null + + if (reviewId) { + // 更新現有評論 + resultReviewId = await appService.updateAppReview(reviewId, appId, userId, rating, comment) + } else { + // 創建新評論 + resultReviewId = await appService.createAppReview(appId, userId, rating, comment) + } + + if (resultReviewId) { + // 獲取更新後的評論列表 + const reviews = await appService.getAppReviews(appId, 10, 0) + return NextResponse.json({ + success: true, + data: { reviewId: resultReviewId, reviews } + }) + } else { + return NextResponse.json( + { success: false, error: reviewId ? '更新評論失敗' : '創建評論失敗' }, + { status: 500 } + ) + } + } catch (error) { + console.error('處理應用評論錯誤:', error) + return NextResponse.json( + { success: false, error: '處理應用評論時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/apps/[id]/stats/route.ts b/app/api/apps/[id]/stats/route.ts index 7874cd5..66e6a25 100644 --- a/app/api/apps/[id]/stats/route.ts +++ b/app/api/apps/[id]/stats/route.ts @@ -6,6 +6,11 @@ const appService = new AppService() export async function GET(request: NextRequest, { params }: { params: { id: string } }) { try { const { id: appId } = await params + const { searchParams } = new URL(request.url) + + // 獲取日期範圍參數 + const startDate = searchParams.get('startDate') + const endDate = searchParams.get('endDate') // 獲取應用基本統計 const app = await appService.getAppById(appId) @@ -19,8 +24,11 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri // 獲取評分統計 const ratingStats = await appService.getAppRatingStats(appId) - // 獲取使用趨勢數據 - const usageStats = await appService.getAppUsageStats(appId) + // 獲取使用趨勢數據(支援日期範圍) + const usageStats = await appService.getAppUsageStats(appId, startDate || undefined, endDate || undefined) + + // 獲取收藏數量 + const favoritesCount = await appService.getAppFavoritesCount(appId) return NextResponse.json({ success: true, @@ -28,6 +36,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri basic: { views: app.views_count || 0, likes: app.likes_count || 0, + favorites: favoritesCount, rating: ratingStats.averageRating || 0, reviewCount: ratingStats.totalRatings || 0 }, diff --git a/app/api/reviews/[id]/votes/route.ts b/app/api/reviews/[id]/votes/route.ts new file mode 100644 index 0000000..c3a8880 --- /dev/null +++ b/app/api/reviews/[id]/votes/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +// 獲取評論的投票統計 +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: reviewId } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + const votes = await appService.getReviewVotes(reviewId, userId || undefined) + + return NextResponse.json({ + success: true, + data: votes + }) + } catch (error) { + console.error('獲取評論投票錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取評論投票時發生錯誤' }, + { status: 500 } + ) + } +} + +// 投票或取消投票 +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id: reviewId } = await params + const { userId, isHelpful } = await request.json() + + if (!userId || typeof isHelpful !== 'boolean') { + return NextResponse.json( + { success: false, error: '缺少必要參數' }, + { status: 400 } + ) + } + + const result = await appService.toggleReviewVote(reviewId, userId, isHelpful) + + if (result.success) { + // 獲取更新後的投票統計 + const votes = await appService.getReviewVotes(reviewId, userId) + return NextResponse.json({ + success: true, + data: votes + }) + } else { + return NextResponse.json( + { success: false, error: result.error || '投票失敗' }, + { status: 500 } + ) + } + } catch (error) { + console.error('投票評論錯誤:', error) + return NextResponse.json( + { success: false, error: '投票評論時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/user/activity/route.ts b/app/api/user/activity/route.ts new file mode 100644 index 0000000..cb70fcd --- /dev/null +++ b/app/api/user/activity/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +// 獲取用戶活動記錄 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json( + { success: false, error: '用戶ID不能為空' }, + { status: 400 } + ) + } + + // 獲取用戶最近使用的應用 + const recentApps = await appService.getUserRecentApps(userId, 10) + + // 獲取用戶統計數據 + const userStats = await appService.getUserActivityStats(userId) + + // 獲取類別使用統計 + const categoryStats = await appService.getUserCategoryStats(userId) + + return NextResponse.json({ + success: true, + data: { + recentApps, + userStats, + categoryStats + } + }) + + } catch (error) { + console.error('獲取用戶活動記錄錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取活動記錄時發生錯誤' }, + { status: 500 } + ) + } +} + +// 記錄用戶活動 +export async function POST(request: NextRequest) { + try { + const { userId, action, resourceType, resourceId, details } = await request.json() + + if (!userId || !action || !resourceType) { + return NextResponse.json( + { success: false, error: '缺少必要參數' }, + { status: 400 } + ) + } + + // 記錄活動到資料庫 + const activityId = await appService.logUserActivity({ + userId, + action, + resourceType, + resourceId, + details + }) + + return NextResponse.json({ + success: true, + data: { activityId } + }) + + } catch (error) { + console.error('記錄用戶活動錯誤:', error) + return NextResponse.json( + { success: false, error: '記錄活動時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/user/favorites/route.ts b/app/api/user/favorites/route.ts new file mode 100644 index 0000000..3a5c60a --- /dev/null +++ b/app/api/user/favorites/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +// 獲取用戶收藏列表 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + const page = parseInt(searchParams.get('page') || '1') + const limit = parseInt(searchParams.get('limit') || '20') + + if (!userId) { + return NextResponse.json( + { success: false, error: '用戶ID不能為空' }, + { status: 400 } + ) + } + + const offset = (page - 1) * limit + const result = await appService.getUserFavorites(userId, limit, offset) + + return NextResponse.json({ + success: true, + data: { + apps: result.apps, + pagination: { + page, + limit, + total: result.total, + totalPages: Math.ceil(result.total / limit) + } + } + }) + + } catch (error) { + console.error('獲取用戶收藏列表錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取收藏列表時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/api/user/interactions/route.ts b/app/api/user/interactions/route.ts new file mode 100644 index 0000000..087caa8 --- /dev/null +++ b/app/api/user/interactions/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server' +import { AppService } from '@/lib/services/database-service' + +const appService = new AppService() + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json( + { success: false, error: '缺少用戶ID' }, + { status: 400 } + ) + } + + // 直接查詢收藏的應用ID(不依賴 apps 表的 is_active 狀態) + const favoriteAppIds = await appService.getUserFavoriteAppIds(userId) + console.log('用戶收藏的應用ID列表:', favoriteAppIds) + + // 獲取用戶的按讚列表 + const likedAppIds = await appService.getUserLikedApps(userId) + console.log('用戶按讚的應用ID列表:', likedAppIds) + + return NextResponse.json({ + success: true, + data: { + favorites: favoriteAppIds, + likes: likedAppIds + } + }) + + } catch (error) { + console.error('獲取用戶互動狀態錯誤:', error) + return NextResponse.json( + { success: false, error: '獲取用戶互動狀態時發生錯誤' }, + { status: 500 } + ) + } +} diff --git a/app/page.tsx b/app/page.tsx index 9ed34b5..9fe3d15 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -198,6 +198,29 @@ export default function AIShowcasePlatform() { setTotalPages(data.data.pagination.totalPages) setTotalApps(data.data.pagination.total) setDepartments(data.data.departments || []) + + // 為每個應用載入統計數據 + if (data.data.apps && data.data.apps.length > 0) { + const updatedApps = await Promise.all( + data.data.apps.map(async (app: any) => { + try { + const userId = user?.id + const statsResponse = await fetch(`/api/apps/${app.id}/interactions${userId ? `?userId=${userId}` : ''}`) + if (statsResponse.ok) { + const statsData = await statsResponse.json() + if (statsData.success) { + console.log(`載入應用 ${app.name} 的統計數據:`, statsData.data) + return { ...app, ...statsData.data } + } + } + } catch (error) { + console.error(`載入應用 ${app.name} 統計數據錯誤:`, error) + } + return app + }) + ) + setAiApps(updatedApps) + } } else { console.error('載入應用數據失敗:', data.error) setAiApps([]) @@ -306,6 +329,13 @@ export default function AIShowcasePlatform() { setShowAppDetail(true) } + const handleAppDetailClose = async () => { + setShowAppDetail(false) + setSelectedApp(null) + // 重新載入應用數據以更新統計信息 + await loadApps() + } + const handleSwitchToForgotPassword = () => { setShowLogin(false) setShowForgotPassword(true) @@ -316,8 +346,8 @@ export default function AIShowcasePlatform() { setShowLogin(true) } - const handleTryApp = (appId: string) => { - incrementViewCount(appId) + const handleTryApp = async (appId: string) => { + await incrementViewCount(appId) addToRecentApps(appId) console.log(`Opening app ${appId}`) } @@ -972,17 +1002,18 @@ export default function AIShowcasePlatform() {
{currentApps.map((app) => { const IconComponent = getIconComponent(app.icon || 'Bot') - const likes = getAppLikes(app.id.toString()) - const views = getViewCount(app.id.toString()) - const rating = getAppRating(app.id.toString()) + const likes = Number(app.likesCount) || 0 + const views = Number(app.viewsCount) || 0 + const rating = Number(app.rating) || 0 + const reviewsCount = Number(app.reviewsCount) || 0 return (
-
- +
+
@@ -991,6 +1022,9 @@ export default function AIShowcasePlatform() {

by {app.creator}

+
+ +
@@ -1007,7 +1041,12 @@ export default function AIShowcasePlatform() {
- +
{views} @@ -1016,9 +1055,12 @@ export default function AIShowcasePlatform() { {rating.toFixed(1)}
+
+ + {reviewsCount} +
-
@@ -262,7 +398,13 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp

發布日期

-

2024年1月15日

+

+ {(app as any).createdAt ? new Date((app as any).createdAt).toLocaleDateString('zh-TW', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) : '未知'} +

@@ -270,10 +412,10 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp

功能特色

    -
  • • 智能化處理能力
  • -
  • • 高效能運算
  • -
  • • 用戶友好介面
  • -
  • • 多語言支援
  • +
  • • 基於 {app.type} 技術
  • +
  • • 適用於 {app.department} 部門
  • +
  • • 提供智能化解決方案
  • +
  • • 支援多種使用場景
@@ -282,28 +424,17 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp

詳細描述

- {app.description} 這個應用採用了最新的人工智能技術, - 為用戶提供了卓越的體驗。無論是在處理複雜任務還是日常工作中, - 都能展現出色的性能和可靠性。我們持續優化和改進, 確保為用戶帶來最佳的使用體驗。 + {app.description || '暫無詳細描述'}

-
- - -
{/* 基本統計數據 */} -
+
總瀏覽量 @@ -311,7 +442,7 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
- {isLoadingStats ? '...' : appStats.basic.views} + {isLoadingStats ? '...' : views}

累計瀏覽次數

@@ -319,12 +450,25 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp - 收藏數 - + 按讚數 +
- {isLoadingStats ? '...' : appStats.basic.likes} + {isLoadingStats ? '...' : likes} +
+

用戶按讚數量

+
+
+ + + + 收藏數 + + + +
+ {isLoadingStats ? '...' : (appStats.basic as any).favorites || 0}

用戶收藏數量

@@ -337,7 +481,7 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
- {isLoadingStats ? '...' : appStats.basic.rating.toFixed(1)} + {isLoadingStats ? '...' : Number(rating).toFixed(1)}

用戶評分

@@ -350,7 +494,7 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
- {isLoadingStats ? '...' : appStats.basic.reviewCount} + {isLoadingStats ? '...' : reviewsCount}

用戶評價總數

@@ -418,6 +562,7 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp value={startDate} onChange={(e) => setStartDate(e.target.value)} className="w-36" + max={endDate} />
@@ -430,103 +575,139 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp value={endDate} onChange={(e) => setEndDate(e.target.value)} className="w-36" + min={startDate} + max={new Date().toISOString().split("T")[0]} />
+
- {/* Chart Container with Horizontal Scroll */} -
-
- {/* Month/Year Section Headers */} -
- {(() => { - 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 ( -
- {section.label} -
- ) - }) - })()} -
- - {/* Chart Bars */} -
- {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 ( -
- {/* Month divider line */} - {isNewMonth && index > 0 && ( -
- )} - -
-
- {/* Value label */} -
- {day.users} -
-
-
- - {/* Consistent day-only labels */} -
{currentDate.getDate()}日
-
- ) - })} + {isLoadingStats ? ( +
+
+
+

載入使用趨勢數據中...

-
+ ) : usageStats.trendData && usageStats.trendData.length > 0 ? ( + <> + {/* Chart Container with Horizontal Scroll */} +
+
+ {/* Month/Year Section Headers */} +
+ {(() => { + const sections = getDateSections(usageStats.trendData) + const totalBars = usageStats.trendData.length - {/* Scroll Hint */} - {usageStats.trendData.length > 20 && ( -
💡 提示:圖表可左右滑動查看更多數據
+ return Object.entries(sections).map(([key, section]) => { + const width = ((section.endIndex - section.startIndex + 1) / totalBars) * 100 + const left = (section.startIndex / totalBars) * 100 + + return ( +
+ {section.label} +
+ ) + }) + })()} +
+ + {/* Chart Bars */} +
+ {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 ( +
+ {/* Month divider line */} + {isNewMonth && index > 0 && ( +
+ )} + +
+
+ {/* Value label */} +
+ {day.users} +
+
+
+ + {/* Consistent day-only labels */} +
{currentDate.getDate()}日
+
+ ) + })} +
+
+
+ + {/* Scroll Hint */} + {usageStats.trendData && usageStats.trendData.length > 20 && ( +
💡 提示:圖表可左右滑動查看更多數據
+ )} + + ) : ( +
+
+ +

在選定的日期範圍內暫無使用數據

+

請嘗試選擇其他日期範圍

+
+
)}
@@ -540,18 +721,30 @@ export function AppDetailDialog({ open, onOpenChange, app }: AppDetailDialogProp
- {usageStats.topDepartments.map((dept) => ( -
-
-
- {dept.name} -
-
- {dept.users} 人 - {dept.percentage}% -
+ {usageStats.topDepartments && usageStats.topDepartments.length > 0 ? ( + 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 + + return ( +
+
+
+ {dept.department || '未知部門'} +
+
+ {dept.count} 人 + {percentage}% +
+
+ ) + }) + ) : ( +
+ +

暫無部門使用數據

- ))} + )}
diff --git a/components/auth/activity-records-dialog.tsx b/components/auth/activity-records-dialog.tsx index 33bab7e..dc8b7d6 100644 --- a/components/auth/activity-records-dialog.tsx +++ b/components/auth/activity-records-dialog.tsx @@ -6,7 +6,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Progress } from "@/components/ui/progress" -import { BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp, Trash2, RefreshCw } from "lucide-react" +import { + BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp, Trash2, RefreshCw, + Bot, Mic, Settings, Zap, Star, Eye, Target, Users, Lightbulb, Search, Database, Shield, Cpu, + Globe, Layers, PieChart, Activity, Calendar, Code, Command, Compass, CreditCard, Download, + Edit, ExternalLink, Filter, Flag, Folder, Gift, Home, Info, Key, Link, Lock, Mail, MapPin, + Minus, Monitor, MoreHorizontal, MoreVertical, MousePointer, Navigation, Pause, Play, Plus, + Power, Save, Send, Share, Smartphone, Tablet, Upload, Volume2, Wifi, X, ZoomIn, ZoomOut +} from "lucide-react" import { useState, useEffect } from "react" interface ActivityRecordsDialogProps { @@ -30,74 +37,47 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia const [recentApps, setRecentApps] = useState([]) const [categoryData, setCategoryData] = useState([]) + const [userStats, setUserStats] = useState({ + totalUsage: 0, + favoriteApps: 0, + daysJoined: 0 + }) + const [isLoading, setIsLoading] = useState(false) const [isResetting, setIsResetting] = useState(false) if (!user) return null - // Calculate user statistics - const calculateUserStats = () => { - if (!user) return { - totalUsage: 0, - totalDuration: 0, - favoriteApps: 0, - daysJoined: 0 - } + // Load user activity data from API + const loadUserActivity = async () => { + if (!user) return - // Calculate total usage count (views) - const totalUsage = Object.values(user.recentApps || []).length - - // Calculate total duration (simplified - 5 minutes per app view) - const totalDuration = totalUsage * 5 // minutes - - // Get favorite apps count - const favoriteApps = user.favoriteApps?.length || 0 - - // Calculate days joined - const joinDate = new Date(user.joinDate) - const now = new Date() - - // Check if joinDate is valid - let daysJoined = 0 - if (!isNaN(joinDate.getTime())) { - daysJoined = Math.floor((now.getTime() - joinDate.getTime()) / (1000 * 60 * 60 * 24)) - } - - return { - totalUsage, - totalDuration, - favoriteApps, - daysJoined: Math.max(0, daysJoined) + setIsLoading(true) + try { + const response = await fetch(`/api/user/activity?userId=${user.id}`) + const data = await response.json() + + if (data.success) { + setRecentApps(data.data.recentApps || []) + setCategoryData(data.data.categoryStats || []) + setUserStats(data.data.userStats || { + totalUsage: 0, + favoriteApps: 0, + daysJoined: 0 + }) + } + } catch (error) { + console.error('載入用戶活動數據錯誤:', error) + } finally { + setIsLoading(false) } } - const stats = calculateUserStats() - - // Load recent apps from user's recent apps + // Load data when dialog opens useEffect(() => { - if (user?.recentApps) { - // Convert recent app IDs to app objects (simplified) - const recentAppsData = user.recentApps.slice(0, 10).map((appId, index) => ({ - id: appId, - name: `應用 ${appId}`, - author: "系統", - category: "AI應用", - usageCount: getViewCount(appId), - timeSpent: "5分鐘", - lastUsed: `${index + 1}天前`, - icon: MessageSquare, - color: "bg-blue-500" - })) - setRecentApps(recentAppsData) - } else { - setRecentApps([]) + if (open && user) { + loadUserActivity() } - }, [user, getViewCount]) - - // Load category data (simplified) - useEffect(() => { - // This would normally be calculated from actual usage data - setCategoryData([]) - }, [user]) + }, [open, user]) // Reset user activity data const resetActivityData = async () => { @@ -155,38 +135,150 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia

最近使用的應用

您最近體驗過的 AI 應用

- {recentApps.length > 0 ? ( + {isLoading ? ( +
+
+

載入活動記錄中...

+
+ ) : recentApps.length > 0 ? (
{recentApps.map((app) => { - const IconComponent = app.icon + // 圖標映射函數 + const getIconComponent = (iconName: string) => { + const iconMap: { [key: string]: any } = { + 'Bot': Bot, + 'ImageIcon': ImageIcon, + 'Mic': Mic, + 'MessageSquare': MessageSquare, + 'Settings': Settings, + 'Zap': Zap, + 'TrendingUp': TrendingUp, + 'Star': Star, + 'Heart': Heart, + 'Eye': Eye, + 'Target': Target, + 'Users': Users, + 'Lightbulb': Lightbulb, + 'Search': Search, + 'BarChart3': BarChart3, + 'Database': Database, + 'Shield': Shield, + 'Cpu': Cpu, + 'FileText': FileText, + 'Globe': Globe, + 'Layers': Layers, + 'PieChart': PieChart, + 'Activity': Activity, + 'Calendar': Calendar, + 'Clock': Clock, + 'Code': Code, + 'Command': Command, + 'Compass': Compass, + 'CreditCard': CreditCard, + 'Download': Download, + 'Edit': Edit, + 'ExternalLink': ExternalLink, + 'Filter': Filter, + 'Flag': Flag, + 'Folder': Folder, + 'Gift': Gift, + 'Home': Home, + 'Info': Info, + 'Key': Key, + 'Link': Link, + 'Lock': Lock, + 'Mail': Mail, + 'MapPin': MapPin, + 'Minus': Minus, + 'Monitor': Monitor, + 'MoreHorizontal': MoreHorizontal, + 'MoreVertical': MoreVertical, + 'MousePointer': MousePointer, + 'Navigation': Navigation, + 'Pause': Pause, + 'Play': Play, + 'Plus': Plus, + 'Power': Power, + 'RefreshCw': RefreshCw, + 'Save': Save, + 'Send': Send, + 'Share': Share, + 'Smartphone': Smartphone, + 'Tablet': Tablet, + 'Trash2': Trash2, + 'Upload': Upload, + 'Volume2': Volume2, + 'Wifi': Wifi, + 'X': X, + 'ZoomIn': ZoomIn, + 'ZoomOut': ZoomOut, + } + return iconMap[iconName] || MessageSquare + } + + const IconComponent = getIconComponent(app.icon || 'MessageSquare') + const lastUsedDate = new Date(app.lastUsed) + const now = new Date() + const diffTime = Math.abs(now.getTime() - lastUsedDate.getTime()) + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + return (
-
+

{app.name}

-

by {app.author}

+

by {app.creator}

- {app.category} + {app.type} 使用 {app.usageCount} 次 - - - {app.timeSpent} -
-

{app.lastUsed}

-
@@ -237,35 +329,16 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
{/* Statistics Cards */} -
+
總使用次數 -
{isNaN(stats.totalUsage) ? 0 : stats.totalUsage}
+
{isNaN(userStats.totalUsage) ? 0 : userStats.totalUsage}

- {(isNaN(stats.totalUsage) ? 0 : stats.totalUsage) > 0 ? "累計使用" : "尚未使用任何應用"} -

-
-
- - - - 使用時長 - - - -
- {isNaN(stats.totalDuration) ? "0分鐘" : ( - stats.totalDuration >= 60 - ? `${(stats.totalDuration / 60).toFixed(1)}小時` - : `${stats.totalDuration}分鐘` - )} -
-

- {(isNaN(stats.totalDuration) ? 0 : stats.totalDuration) > 0 ? "累計時長" : "尚未開始使用"} + {(isNaN(userStats.totalUsage) ? 0 : userStats.totalUsage) > 0 ? "累計使用" : "尚未使用任何應用"}

@@ -276,9 +349,9 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia -
{isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps}
+
{isNaN(userStats.favoriteApps) ? 0 : userStats.favoriteApps}

- {(isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps) > 0 ? "個人收藏" : "尚未收藏任何應用"} + {(isNaN(userStats.favoriteApps) ? 0 : userStats.favoriteApps) > 0 ? "個人收藏" : "尚未收藏任何應用"}

@@ -289,9 +362,9 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia -
{isNaN(stats.daysJoined) ? 0 : stats.daysJoined}
+
{isNaN(userStats.daysJoined) ? 0 : userStats.daysJoined}

- {(isNaN(stats.daysJoined) ? 0 : stats.daysJoined) > 0 ? "已加入平台" : "今天剛加入"} + {(isNaN(userStats.daysJoined) ? 0 : userStats.daysJoined) > 0 ? "已加入平台" : "今天剛加入"}

diff --git a/components/favorite-button.tsx b/components/favorite-button.tsx index 496e55e..0c5bd8d 100644 --- a/components/favorite-button.tsx +++ b/components/favorite-button.tsx @@ -1,6 +1,7 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" +import { useAuth } from "@/contexts/auth-context" import { Button } from "@/components/ui/button" import { Heart } from "lucide-react" import { cn } from "@/lib/utils" @@ -20,17 +21,31 @@ export function FavoriteButton({ size = "md", variant = "ghost", }: FavoriteButtonProps) { + const { user, isFavorite, toggleFavorite } = useAuth() const [isFavorited, setIsFavorited] = useState(initialFavorited) const [isLoading, setIsLoading] = useState(false) + // 使用 isFavorite 的實時結果 + const currentFavorited = user ? isFavorite(appId) : isFavorited + + // 載入收藏狀態 + useEffect(() => { + if (user) { + const favorited = isFavorite(appId) + setIsFavorited(favorited) + } + }, [user, appId, isFavorite]) + const handleToggle = async () => { + if (!user) { + console.warn('用戶未登入,無法收藏') + return + } + setIsLoading(true) try { - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 300)) - - const newFavoriteState = !isFavorited + const newFavoriteState = await toggleFavorite(appId) setIsFavorited(newFavoriteState) // Call the callback if provided @@ -58,16 +73,16 @@ export function FavoriteButton({ -
- - - ))} + {/* Loading State */} + {isLoading && ( +
+
+

載入收藏列表中...

- ) : ( + )} + + {/* Apps Grid */} + {!isLoading && ( +
+ {filteredAndSortedApps.map((app) => { + const IconComponent = getIconComponent(app.icon || 'Heart') + return ( + + +
+
+
+ +
+
+

+ {app.name} +

+

by {app.creator}

+
+
+ +
+ +

{app.description}

+ +
+ + {app.type} + + {app.department} +
+ + {/* 統計數據區塊 - 優化佈局 */} +
+ {/* 評分 - 突出顯示 */} +
+ + {Number(app.rating).toFixed(1)} + 評分 +
+ + {/* 其他統計數據 - 3列佈局 */} +
+
+ + {app.views} + 瀏覽 +
+
+ + {app.likes} + 按讚 +
+
+ + {app.reviewCount} + 評論 +
+
+ + {/* 使用應用按鈕 */} + +
+
+
+ ) + })} +
+ )} + + {/* Empty State */} + {!isLoading && filteredAndSortedApps.length === 0 && (
-

暫無收藏應用

-

- {filterDepartment !== "all" - ? "該部門暫無收藏的應用,請嘗試其他篩選條件" - : "您還沒有收藏任何應用,快去探索並收藏您喜歡的 AI 應用吧!"} -

+

還沒有收藏任何應用

+

開始探索並收藏您喜歡的 AI 應用吧!

+
+ )} + + {/* Pagination */} + {!isLoading && totalPages > 1 && ( +
+ + + 第 {currentPage} 頁,共 {totalPages} 頁 + +
)}
) -} +} \ No newline at end of file diff --git a/components/like-button.tsx b/components/like-button.tsx index c58025d..d0f44ff 100644 --- a/components/like-button.tsx +++ b/components/like-button.tsx @@ -2,7 +2,7 @@ import type React from "react" -import { useState } from "react" +import { useState, useEffect } from "react" import { ThumbsUp } from "lucide-react" import { Button } from "@/components/ui/button" import { useAuth } from "@/contexts/auth-context" @@ -14,15 +14,40 @@ interface LikeButtonProps { size?: "sm" | "default" | "lg" className?: string showCount?: boolean + likeCount?: number + userLiked?: boolean } -export function LikeButton({ appId, size = "default", className, showCount = true }: LikeButtonProps) { - const { user, likeApp, getAppLikes, hasLikedToday } = useAuth() +export function LikeButton({ appId, size = "default", className, showCount = true, likeCount: propLikeCount, userLiked: propUserLiked }: LikeButtonProps) { + const { user, toggleLike, getAppLikes, isLiked } = useAuth() const { toast } = useToast() const [isLoading, setIsLoading] = useState(false) + const [localLikeCount, setLocalLikeCount] = useState(propLikeCount || 0) + const [localUserLiked, setLocalUserLiked] = useState(propUserLiked || false) - const likeCount = getAppLikes(appId) - const hasLiked = user ? hasLikedToday(appId) : false + const likeCount = localLikeCount + const hasLiked = user ? isLiked(appId) : localUserLiked + + // 載入用戶的按讚狀態 + useEffect(() => { + if (user) { + const liked = isLiked(appId) + setLocalUserLiked(liked) + } + }, [user, appId, isLiked]) + + // 同步外部 props 變化 + useEffect(() => { + if (propLikeCount !== undefined) { + setLocalLikeCount(propLikeCount) + } + }, [propLikeCount]) + + useEffect(() => { + if (propUserLiked !== undefined) { + setLocalUserLiked(propUserLiked) + } + }, [propUserLiked]) const handleLike = async (e: React.MouseEvent) => { e.stopPropagation() @@ -36,25 +61,37 @@ export function LikeButton({ appId, size = "default", className, showCount = tru return } - if (hasLiked) { - toast({ - title: "今日已按讚", - description: "您今天已經為這個應用按過讚了", - variant: "destructive", - }) - return - } - setIsLoading(true) + + // 立即更新本地狀態 + const newLikedState = !hasLiked + const newLikeCount = hasLiked ? likeCount - 1 : likeCount + 1 + + setLocalUserLiked(newLikedState) + setLocalLikeCount(newLikeCount) + try { - await likeApp(appId) - toast({ - title: "按讚成功!", - description: "感謝您的支持", - }) + const success = await toggleLike(appId) + if (newLikedState) { + // 剛剛按讚了 + toast({ + title: "按讚成功!", + description: "感謝您的支持", + }) + } else { + // 剛剛取消按讚了 + toast({ + title: "取消按讚", + description: "已取消對該應用的按讚", + }) + } } catch (error) { + console.error("按讚操作失敗:", error) + // 如果操作失敗,回滾本地狀態 + setLocalUserLiked(hasLiked) + setLocalLikeCount(likeCount) toast({ - title: "按讚失敗", + title: "操作失敗", description: "請稍後再試", variant: "destructive", }) @@ -85,7 +122,7 @@ export function LikeButton({ appId, size = "default", className, showCount = tru sizeClasses[size], "flex items-center", hasLiked - ? "text-blue-600 hover:text-blue-700 hover:bg-blue-50" + ? "text-blue-600 bg-blue-50 border-blue-200 hover:text-blue-700 hover:bg-blue-100" : "text-gray-500 hover:text-blue-600 hover:bg-blue-50", "transition-all duration-200", className, diff --git a/components/reviews/review-system-fixed.tsx b/components/reviews/review-system-fixed.tsx new file mode 100644 index 0000000..4294af8 --- /dev/null +++ b/components/reviews/review-system-fixed.tsx @@ -0,0 +1,496 @@ +"use client" + +import { useState, useEffect } from "react" +import { useAuth } from "@/contexts/auth-context" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Separator } from "@/components/ui/separator" +import { Star, MessageSquare, ThumbsUp, ThumbsDown, Edit, Trash2, MoreHorizontal } from "lucide-react" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" + +interface Review { + id: string + userId: string + userName: string + userAvatar?: string + userDepartment: string + rating: number + comment: string + createdAt: string + updatedAt?: string + helpful: number + notHelpful: number + userHelpfulVotes: string[] // user IDs who voted helpful + userNotHelpfulVotes: string[] // user IDs who voted not helpful +} + +interface ReviewSystemProps { + appId: string + appName: string + currentRating: number + onRatingUpdate: (newRating: number, reviewCount: number) => void +} + +export function ReviewSystem({ appId, appName, currentRating, onRatingUpdate }: ReviewSystemProps) { + const { user, updateAppRating } = useAuth() + + // Load reviews from database + const [reviews, setReviews] = useState([]) + const [isLoadingReviews, setIsLoadingReviews] = useState(true) + + const [showReviewForm, setShowReviewForm] = useState(false) + const [newRating, setNewRating] = useState(5) + const [newComment, setNewComment] = useState("") + const [isSubmitting, setIsSubmitting] = useState(false) + const [editingReview, setEditingReview] = useState(null) + const [sortBy, setSortBy] = useState<"newest" | "oldest" | "helpful">("newest") + + // Load reviews from database + const loadReviews = async () => { + try { + setIsLoadingReviews(true) + const response = await fetch(`/api/apps/${appId}/reviews?limit=100`) // 載入更多評論 + if (response.ok) { + const data = await response.json() + if (data.success) { + // Transform database format to component format + const transformedReviews = data.data.reviews.map((review: any) => ({ + id: review.id, + userId: review.user_id || 'unknown', + userName: review.userName || review.user_name || '未知用戶', + userAvatar: review.userAvatar || review.user_avatar, + userDepartment: review.userDepartment || review.user_department || '未知部門', + rating: review.rating, + comment: review.review || review.comment || '', + createdAt: review.ratedAt || review.rated_at || new Date().toISOString(), + helpful: 0, + notHelpful: 0, + userHelpfulVotes: [], + userNotHelpfulVotes: [], + })) + setReviews(transformedReviews) + } + } + } catch (error) { + console.error('載入評論錯誤:', error) + } finally { + setIsLoadingReviews(false) + } + } + + // Load reviews on component mount + useEffect(() => { + loadReviews() + }, [appId]) + + const userReview = reviews.find((review) => review.userId === user?.id) + const canReview = user && !userReview + + // Update rating when reviews change + useEffect(() => { + if (reviews.length > 0) { + const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length + const newAvgRating = Number(avgRating.toFixed(1)) + updateAppRating(appId, newAvgRating) + onRatingUpdate(newAvgRating, reviews.length) + } else { + updateAppRating(appId, 0) + onRatingUpdate(0, 0) + } + }, [reviews, appId, updateAppRating, onRatingUpdate]) + + const handleSubmitReview = async () => { + if (!user || !newComment.trim()) return + + setIsSubmitting(true) + + try { + const response = await fetch(`/api/apps/${appId}/reviews`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userId: user.id, + rating: newRating, + comment: newComment.trim(), + }), + }) + + if (response.ok) { + const data = await response.json() + if (data.success) { + // Reload reviews from database + await loadReviews() + setNewComment("") + setNewRating(5) + setShowReviewForm(false) + } else { + console.error('提交評論失敗:', data.error) + alert('提交評論失敗,請稍後再試') + } + } else { + console.error('提交評論失敗:', response.statusText) + alert('提交評論失敗,請稍後再試') + } + } catch (error) { + console.error('提交評論錯誤:', error) + alert('提交評論時發生錯誤,請稍後再試') + } finally { + setIsSubmitting(false) + } + } + + const handleEditReview = async (reviewId: string) => { + if (!user || !newComment.trim()) return + + setIsSubmitting(true) + + try { + const response = await fetch(`/api/apps/${appId}/reviews`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userId: user.id, + rating: newRating, + comment: newComment.trim(), + }), + }) + + if (response.ok) { + const data = await response.json() + if (data.success) { + // Reload reviews from database + await loadReviews() + setEditingReview(null) + setNewComment("") + setNewRating(5) + } else { + console.error('更新評論失敗:', data.error) + alert('更新評論失敗,請稍後再試') + } + } else { + console.error('更新評論失敗:', response.statusText) + alert('更新評論失敗,請稍後再試') + } + } catch (error) { + console.error('更新評論錯誤:', error) + alert('更新評論時發生錯誤,請稍後再試') + } finally { + setIsSubmitting(false) + } + } + + const handleDeleteReview = async (reviewId: string) => { + // For now, we'll just reload reviews since we don't have a delete API yet + // In a real implementation, you would call a DELETE API endpoint + await loadReviews() + } + + const handleHelpfulVote = (reviewId: string, isHelpful: boolean) => { + if (!user) return + + // For now, we'll just reload reviews since we don't have a helpful vote API yet + // In a real implementation, you would call an API endpoint to handle helpful votes + loadReviews() + } + + const startEdit = (review: Review) => { + setEditingReview(review.id) + setNewRating(review.rating) + setNewComment(review.comment) + setShowReviewForm(true) + } + + const cancelEdit = () => { + setEditingReview(null) + setNewComment("") + setNewRating(5) + setShowReviewForm(false) + } + + const getInitials = (name: string) => { + return name.split("").slice(0, 2).join("").toUpperCase() + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("zh-TW", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }) + } + + const renderStars = (rating: number, interactive = false, onRate?: (rating: number) => void) => { + return ( +
+ {[1, 2, 3, 4, 5].map((star) => ( + interactive && onRate && onRate(star)} + /> + ))} +
+ ) + } + + const sortedReviews = [...reviews].sort((a, b) => { + switch (sortBy) { + case "oldest": + return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + case "helpful": + return b.helpful - a.helpful + case "newest": + default: + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + } + }) + + return ( +
+ {/* Review Summary */} + + + + + 用戶評價 + + + {isLoadingReviews ? ( + "載入評價中..." + ) : reviews.length > 0 ? ( +
+
+ {renderStars(Math.round(currentRating))} + {Number(currentRating).toFixed(1)} + ({reviews.length} 則評價) +
+
+ ) : ( + "尚無評價,成為第一個評價的用戶!" + )} +
+
+ + {/* Rating Distribution */} + {reviews.length > 0 && ( +
+ {[5, 4, 3, 2, 1].map((rating) => { + const count = reviews.filter((r) => r.rating === rating).length + const percentage = (count / reviews.length) * 100 + return ( +
+
+ {rating} + +
+
+
+
+ {count} +
+ ) + })} +
+ )} + + {/* Add Review Button */} + {canReview && ( + + )} + + {userReview && ( + + + 您已經評價過此應用。您可以編輯或刪除您的評價。 + + )} + + + + {/* Review Form */} + + + + {editingReview ? "編輯評價" : "撰寫評價"} + 分享您對 {appName} 的使用體驗 + + +
+
+ + {renderStars(newRating, true, setNewRating)} +
+ +
+ +