實作前台 APP 呈現、APP 詳細頁面

This commit is contained in:
2025-09-10 00:27:26 +08:00
parent 900e33aefa
commit bc2104d374
17 changed files with 318 additions and 1565 deletions

71
app/api/apps/route.ts Normal file
View File

@@ -0,0 +1,71 @@
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 page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '6')
const search = searchParams.get('search') || ''
const category = searchParams.get('category') || 'all'
const type = searchParams.get('type') || 'all'
const department = searchParams.get('department') || 'all'
// 獲取應用列表(只顯示已發布的應用)
const { apps, total } = await appService.getAllApps({
search,
category,
type,
status: 'published', // 只顯示已發布的應用
page,
limit
})
// 獲取部門列表用於篩選
const departments = await appService.getAppDepartments()
return NextResponse.json({
success: true,
data: {
apps: apps.map(app => ({
id: app.id,
name: app.name,
description: app.description,
category: app.category,
type: app.type,
views: app.views_count || 0,
likes: app.likes_count || 0,
rating: app.rating || 0,
reviewCount: app.reviewCount || 0,
creator: app.creator_name || '未知',
department: app.creator_department || '未知',
createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-',
icon: app.icon || 'Bot',
iconColor: app.icon_color || 'from-blue-500 to-purple-500',
appUrl: app.app_url,
isActive: app.is_active
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
},
departments: departments.map(dept => ({
value: dept.department,
label: dept.department,
count: dept.count
}))
}
})
} catch (error) {
console.error('獲取應用列表錯誤:', error)
return NextResponse.json(
{ success: false, error: '獲取應用列表時發生錯誤' },
{ status: 500 }
)
}
}

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { useAuth } from "@/contexts/auth-context"
import { useCompetition } from "@/contexts/competition-context"
import {
@@ -47,8 +47,7 @@ import { AwardDetailDialog } from "@/components/competition/award-detail-dialog"
import { LikeButton } from "@/components/like-button"
// AI applications data - empty for production
const aiApps: any[] = []
// AI applications data - will be loaded from API
// Pagination component
interface PaginationProps {
@@ -156,6 +155,15 @@ export default function AIShowcasePlatform() {
const [showCompetition, setShowCompetition] = useState(false)
const appsPerPage = 6
// 新增狀態管理
const [aiApps, setAiApps] = useState<any[]>([])
const [isLoadingApps, setIsLoadingApps] = useState(true)
const [totalPages, setTotalPages] = useState(0)
const [totalApps, setTotalApps] = useState(0)
const [departments, setDepartments] = useState<{value: string, label: string, count: number}[]>([])
const [types, setTypes] = useState<{value: string, label: string, count: number}[]>([])
const [categories, setCategories] = useState<{value: string, label: string, count: number}[]>([])
// Competition page states
const [selectedCompetitionTypeFilter, setSelectedCompetitionTypeFilter] = useState("all")
const [selectedMonthFilter, setSelectedMonthFilter] = useState("all")
@@ -170,21 +178,61 @@ export default function AIShowcasePlatform() {
const [showAwardDetail, setShowAwardDetail] = useState(false)
const [selectedAward, setSelectedAward] = useState<any>(null)
const filteredApps = aiApps.filter((app) => {
const matchesSearch =
app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.description.toLowerCase().includes(searchTerm.toLowerCase())
const matchesDepartment = selectedDepartment === "all" || app.department === selectedDepartment
const matchesType = selectedType === "all" || app.type === selectedType
// 載入應用數據
const loadApps = async () => {
try {
setIsLoadingApps(true)
const params = new URLSearchParams({
page: currentPage.toString(),
limit: appsPerPage.toString(),
search: searchTerm,
department: selectedDepartment,
type: selectedType
})
return matchesSearch && matchesDepartment && matchesType
})
const response = await fetch(`/api/apps?${params}`)
const data = await response.json()
// Pagination logic
const totalPages = Math.ceil(filteredApps.length / appsPerPage)
const startIndex = (currentPage - 1) * appsPerPage
const endIndex = startIndex + appsPerPage
const currentApps = filteredApps.slice(startIndex, endIndex)
if (data.success) {
setAiApps(data.data.apps)
setTotalPages(data.data.pagination.totalPages)
setTotalApps(data.data.pagination.total)
setDepartments(data.data.departments || [])
} else {
console.error('載入應用數據失敗:', data.error)
setAiApps([])
}
} catch (error) {
console.error('載入應用數據錯誤:', error)
setAiApps([])
} finally {
setIsLoadingApps(false)
}
}
// 載入篩選選項
const loadFilterOptions = async () => {
try {
const response = await fetch('/api/apps?page=1&limit=1')
const data = await response.json()
if (data.success) {
setDepartments(data.data.departments || [])
// 可以添加更多篩選選項的載入
}
} catch (error) {
console.error('載入篩選選項錯誤:', error)
}
}
// 初始載入
useEffect(() => {
loadApps()
loadFilterOptions()
}, [currentPage, searchTerm, selectedDepartment, selectedType])
// 當前顯示的應用(已經由 API 處理分頁和篩選)
const currentApps = aiApps
// Reset to first page when filters change
const handleFilterChange = (filterType: string, value: string) => {
@@ -201,6 +249,40 @@ export default function AIShowcasePlatform() {
setSearchTerm(value)
}
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
// 圖標映射函數
const getIconComponent = (iconName: string) => {
const iconMap: { [key: string]: any } = {
'Bot': Brain,
'ImageIcon': ImageIcon,
'Mic': Mic,
'MessageSquare': MessageSquare,
'Settings': Settings,
'Zap': Zap,
'TrendingUp': TrendingUp,
'Star': Star,
'Heart': Heart,
'Eye': Eye,
'Trophy': Trophy,
'Award': Award,
'Medal': Medal,
'Target': Target,
'Users': Users,
'Lightbulb': Lightbulb,
'Search': Search,
'Plus': Plus,
'X': X,
'ChevronLeft': ChevronLeft,
'ChevronRight': ChevronRight,
'ArrowLeft': ArrowLeft
}
return iconMap[iconName] || Brain // 預設使用 Brain 圖標
}
const getTypeColor = (type: string) => {
const colors = {
: "bg-blue-100 text-blue-800 border-blue-200",
@@ -866,19 +948,30 @@ export default function AIShowcasePlatform() {
{/* Results summary */}
<div className="mt-4 text-sm text-gray-600">
{filteredApps.length} {currentPage} {totalPages}
{totalApps} {currentPage} {totalPages}
</div>
</div>
{/* All Apps with Pagination */}
<div>
<h3 className="text-2xl font-bold text-gray-900 mb-6"> ({filteredApps.length})</h3>
<h3 className="text-2xl font-bold text-gray-900 mb-6">
({totalApps})
{isLoadingApps && <span className="text-sm text-gray-500 ml-2">...</span>}
</h3>
{currentApps.length > 0 ? (
{isLoadingApps ? (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
<Search className="w-16 h-16 mx-auto animate-pulse" />
</div>
<h3 className="text-xl font-semibold text-gray-600 mb-2">...</h3>
<p className="text-gray-500"></p>
</div>
) : currentApps.length > 0 ? (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{currentApps.map((app) => {
const IconComponent = app.icon
const IconComponent = getIconComponent(app.icon || 'Bot')
const likes = getAppLikes(app.id.toString())
const views = getViewCount(app.id.toString())
const rating = getAppRating(app.id.toString())
@@ -944,7 +1037,7 @@ export default function AIShowcasePlatform() {
{/* Pagination */}
{totalPages > 1 && (
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} />
)}
</>
) : (