實作前台 APP 呈現、APP 詳細頁面
This commit is contained in:
71
app/api/apps/route.ts
Normal file
71
app/api/apps/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
135
app/page.tsx
135
app/page.tsx
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
Reference in New Issue
Block a user