From b4386dc481dd7de88cd9690b5dc790a4f5db0fe7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=90=B3=E4=BD=A9=E5=BA=AD?=
Date: Tue, 16 Sep 2025 14:57:40 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=AB=B6=E8=B3=BD=E5=89=8D?=
=?UTF-8?q?=E5=8F=B0=E5=91=88=E7=8F=BE=E3=80=81=E5=88=AA=E9=99=A4=E7=AB=B6?=
=?UTF-8?q?=E8=B3=BD=E3=80=81=E4=BF=AE=E6=94=B9=E7=AB=B6=E8=B3=BD=E7=8B=80?=
=?UTF-8?q?=E6=85=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/api/admin/competitions/[id]/route.ts | 8 +-
app/api/admin/competitions/current/route.ts | 109 ++++++++
app/api/admin/competitions/route.ts | 2 +-
app/api/admin/competitions/stats/route.ts | 3 +-
app/api/competitions/[id]/apps/route.ts | 130 +++++++++
app/api/competitions/[id]/judges/route.ts | 54 ++++
app/api/competitions/[id]/teams/route.ts | 252 ++++++++++++++++++
app/api/competitions/current/route.ts | 38 +++
app/api/competitions/route.ts | 43 +++
app/api/debug/competitions/route.ts | 45 ++++
app/competition/page.tsx | 5 +-
components/admin/competition-management.tsx | 121 ++++++++-
.../competition/popularity-rankings.tsx | 165 ++++++++----
components/competition/team-detail-dialog.tsx | 110 ++++++--
contexts/competition-context.tsx | 18 ++
database-schema-simple.sql | 3 +-
lib/database-failover.js | 16 +-
lib/database-sync-fixed.js | 241 +++++++++++++++++
lib/database-sync-fixed.ts | 241 +++++++++++++++++
lib/services/database-service.ts | 227 ++++++++++++++--
package.json | 10 -
21 files changed, 1714 insertions(+), 127 deletions(-)
create mode 100644 app/api/admin/competitions/current/route.ts
create mode 100644 app/api/competitions/[id]/apps/route.ts
create mode 100644 app/api/competitions/[id]/judges/route.ts
create mode 100644 app/api/competitions/[id]/teams/route.ts
create mode 100644 app/api/competitions/current/route.ts
create mode 100644 app/api/competitions/route.ts
create mode 100644 app/api/debug/competitions/route.ts
diff --git a/app/api/admin/competitions/[id]/route.ts b/app/api/admin/competitions/[id]/route.ts
index c85fdb4..47d116a 100644
--- a/app/api/admin/competitions/[id]/route.ts
+++ b/app/api/admin/competitions/[id]/route.ts
@@ -88,7 +88,7 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
// 驗證狀態(如果提供)
if (body.status) {
- const validStatuses = ['upcoming', 'active', 'judging', 'completed'];
+ const validStatuses = ['upcoming', 'ongoing', 'active', 'judging', 'completed'];
if (!validStatuses.includes(body.status)) {
return NextResponse.json({
success: false,
@@ -160,7 +160,7 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
}
}
-// 刪除競賽(軟刪除)
+// 刪除競賽
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
try {
const { id } = await params;
@@ -175,8 +175,8 @@ export async function DELETE(request: NextRequest, { params }: { params: { id: s
}, { status: 404 });
}
- // 軟刪除:將 is_active 設為 false
- const success = await CompetitionService.updateCompetition(id, { is_active: false });
+ // 使用雙寫功能刪除競賽
+ const success = await CompetitionService.deleteCompetition(id);
if (!success) {
return NextResponse.json({
diff --git a/app/api/admin/competitions/current/route.ts b/app/api/admin/competitions/current/route.ts
new file mode 100644
index 0000000..a21db00
--- /dev/null
+++ b/app/api/admin/competitions/current/route.ts
@@ -0,0 +1,109 @@
+// =====================================================
+// 當前競賽管理 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取當前競賽
+export async function GET(request: NextRequest) {
+ try {
+ const currentCompetition = await CompetitionService.getCurrentCompetition();
+
+ return NextResponse.json({
+ success: true,
+ message: '當前競賽獲取成功',
+ data: currentCompetition
+ });
+
+ } catch (error) {
+ console.error('獲取當前競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取當前競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 設置當前競賽
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { competitionId } = body;
+
+ if (!competitionId) {
+ return NextResponse.json({
+ success: false,
+ message: '缺少競賽ID',
+ error: 'competitionId 為必填欄位'
+ }, { status: 400 });
+ }
+
+ // 檢查競賽是否存在
+ const competition = await CompetitionService.getCompetitionById(competitionId);
+ if (!competition) {
+ return NextResponse.json({
+ success: false,
+ message: '競賽不存在',
+ error: '找不到指定的競賽'
+ }, { status: 404 });
+ }
+
+ // 設置為當前競賽
+ const success = await CompetitionService.setCurrentCompetition(competitionId);
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '設置當前競賽失敗',
+ error: '無法設置當前競賽'
+ }, { status: 500 });
+ }
+
+ // 獲取更新後的當前競賽
+ const updatedCurrentCompetition = await CompetitionService.getCurrentCompetition();
+
+ return NextResponse.json({
+ success: true,
+ message: '當前競賽設置成功',
+ data: updatedCurrentCompetition
+ });
+
+ } catch (error) {
+ console.error('設置當前競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '設置當前競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
+
+// 取消當前競賽
+export async function DELETE(request: NextRequest) {
+ try {
+ const success = await CompetitionService.clearCurrentCompetition();
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: '取消當前競賽失敗',
+ error: '無法取消當前競賽'
+ }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: '當前競賽已取消'
+ });
+
+ } catch (error) {
+ console.error('取消當前競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '取消當前競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/admin/competitions/route.ts b/app/api/admin/competitions/route.ts
index 394b5f2..45a6d82 100644
--- a/app/api/admin/competitions/route.ts
+++ b/app/api/admin/competitions/route.ts
@@ -103,7 +103,7 @@ export async function POST(request: NextRequest) {
}
// 驗證狀態
- const validStatuses = ['upcoming', 'active', 'judging', 'completed'];
+ const validStatuses = ['upcoming', 'ongoing', 'active', 'judging', 'completed'];
if (!validStatuses.includes(status)) {
return NextResponse.json({
success: false,
diff --git a/app/api/admin/competitions/stats/route.ts b/app/api/admin/competitions/stats/route.ts
index 9f46d74..b540e4c 100644
--- a/app/api/admin/competitions/stats/route.ts
+++ b/app/api/admin/competitions/stats/route.ts
@@ -14,7 +14,8 @@ export async function GET(request: NextRequest) {
const stats = {
total: competitions.length,
upcoming: competitions.filter(c => c.status === 'upcoming').length,
- active: competitions.filter(c => c.status === 'active').length,
+ active: competitions.filter(c => c.status === 'active' || c.status === 'ongoing').length,
+ ongoing: competitions.filter(c => c.status === 'ongoing').length,
judging: competitions.filter(c => c.status === 'judging').length,
completed: competitions.filter(c => c.status === 'completed').length,
individual: competitions.filter(c => c.type === 'individual').length,
diff --git a/app/api/competitions/[id]/apps/route.ts b/app/api/competitions/[id]/apps/route.ts
new file mode 100644
index 0000000..674eaef
--- /dev/null
+++ b/app/api/competitions/[id]/apps/route.ts
@@ -0,0 +1,130 @@
+// =====================================================
+// 競賽參賽應用 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+import { AppService } from '@/lib/services/database-service';
+
+// 獲取競賽的參賽應用
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const { searchParams } = new URL(request.url);
+
+ const search = searchParams.get('search') || '';
+ const category = searchParams.get('category') || 'all';
+ const type = searchParams.get('type') || 'all';
+ const department = searchParams.get('department') || 'all';
+ const competitionType = searchParams.get('competitionType') || 'all';
+
+ // 獲取競賽信息
+ const competition = await CompetitionService.getCompetitionWithDetails(id);
+ if (!competition) {
+ return NextResponse.json({
+ success: false,
+ message: '競賽不存在',
+ error: '找不到指定的競賽'
+ }, { status: 404 });
+ }
+
+ // 獲取競賽的參賽應用
+ const competitionApps = await CompetitionService.getCompetitionApps(id);
+
+ if (competitionApps.length === 0) {
+ return NextResponse.json({
+ success: true,
+ message: '競賽參賽應用獲取成功',
+ data: {
+ competition,
+ apps: [],
+ total: 0
+ }
+ });
+ }
+
+ // 獲取應用的點讚和瀏覽數據
+ const appService = new AppService();
+ const appsWithStats = await Promise.all(
+ competitionApps.map(async (app) => {
+ try {
+ const likes = await appService.getAppLikesCount(app.id);
+ const views = await appService.getAppViewsCount(app.id);
+
+ return {
+ ...app,
+ likes,
+ views,
+ competitionType: competition.type // 添加競賽類型
+ };
+ } catch (error) {
+ console.error(`獲取應用 ${app.id} 統計數據失敗:`, error);
+ return {
+ ...app,
+ likes: 0,
+ views: 0,
+ competitionType: competition.type
+ };
+ }
+ })
+ );
+
+ // 過濾掉無效的應用
+ const validApps = appsWithStats.filter(app => app !== null);
+
+ // 應用篩選
+ let filteredApps = validApps.filter(app => {
+ const matchesSearch = search === '' ||
+ app.name.toLowerCase().includes(search.toLowerCase()) ||
+ app.description.toLowerCase().includes(search.toLowerCase()) ||
+ app.creator_name?.toLowerCase().includes(search.toLowerCase());
+
+ const matchesCategory = category === 'all' || app.category === category;
+ const matchesType = type === 'all' || app.type === type;
+ const matchesDepartment = department === 'all' || app.creator_department === department;
+ const matchesCompetitionType = competitionType === 'all' ||
+ (competitionType === 'individual' && app.competition_type === 'individual') ||
+ (competitionType === 'team' && app.competition_type === 'team') ||
+ (competitionType === 'proposal' && app.competition_type === 'proposal');
+
+ return matchesSearch && matchesCategory && matchesType && matchesDepartment && matchesCompetitionType;
+ });
+
+ // 按人氣排序(按讚數)
+ filteredApps.sort((a, b) => (b.likes || 0) - (a.likes || 0));
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽參賽應用獲取成功',
+ data: {
+ competition,
+ apps: filteredApps.map(app => ({
+ id: app.id,
+ name: app.name,
+ description: app.description,
+ category: app.category,
+ type: app.type,
+ views: app.views || 0,
+ likes: app.likes || 0,
+ rating: app.rating || 0,
+ creator: app.creator_name || '未知',
+ department: app.creator_department || '未知',
+ teamName: app.team_name || null,
+ 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',
+ competitionType: app.competitionType || 'individual'
+ })),
+ total: filteredApps.length
+ }
+ });
+
+ } catch (error) {
+ console.error('獲取競賽參賽應用失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽參賽應用失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/competitions/[id]/judges/route.ts b/app/api/competitions/[id]/judges/route.ts
new file mode 100644
index 0000000..097db5b
--- /dev/null
+++ b/app/api/competitions/[id]/judges/route.ts
@@ -0,0 +1,54 @@
+// =====================================================
+// 競賽評審團 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取競賽的評審團
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+
+ // 獲取競賽信息
+ const competition = await CompetitionService.getCompetitionWithDetails(id);
+ if (!competition) {
+ return NextResponse.json({
+ success: false,
+ message: '競賽不存在',
+ error: '找不到指定的競賽'
+ }, { status: 404 });
+ }
+
+ // 獲取競賽的評審團
+ const judges = await CompetitionService.getCompetitionJudges(id);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽評審團獲取成功',
+ data: {
+ competition,
+ judges: judges.map(judge => ({
+ id: judge.id,
+ name: judge.name,
+ title: judge.title,
+ department: judge.department,
+ expertise: judge.expertise || [],
+ avatar: judge.avatar || null,
+ email: judge.email,
+ phone: judge.phone,
+ assignedAt: judge.assigned_at ? new Date(judge.assigned_at).toLocaleDateString('zh-TW') : null
+ })),
+ total: judges.length
+ }
+ });
+
+ } catch (error) {
+ console.error('獲取競賽評審團失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽評審團失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/competitions/[id]/teams/route.ts b/app/api/competitions/[id]/teams/route.ts
new file mode 100644
index 0000000..a0d608c
--- /dev/null
+++ b/app/api/competitions/[id]/teams/route.ts
@@ -0,0 +1,252 @@
+// =====================================================
+// 競賽參賽團隊 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+import { TeamService } from '@/lib/services/database-service';
+import { db } from '@/lib/database';
+
+// 獲取競賽的參賽團隊
+export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
+ try {
+ const { id } = await params;
+ const { searchParams } = new URL(request.url);
+
+ const search = searchParams.get('search') || '';
+ const department = searchParams.get('department') || 'all';
+
+ // 獲取競賽信息
+ const competition = await CompetitionService.getCompetitionWithDetails(id);
+ if (!competition) {
+ return NextResponse.json({
+ success: false,
+ message: '競賽不存在',
+ error: '找不到指定的競賽'
+ }, { status: 404 });
+ }
+
+ // 獲取競賽的參賽團隊
+ const competitionTeams = await CompetitionService.getCompetitionTeams(id);
+
+ if (competitionTeams.length === 0) {
+ return NextResponse.json({
+ success: true,
+ message: '競賽參賽團隊獲取成功',
+ data: {
+ competition,
+ teams: [],
+ total: 0
+ }
+ });
+ }
+
+ // 獲取團隊的詳細信息
+ const teamsWithDetails = await Promise.all(
+ competitionTeams.map(async (team) => {
+ try {
+ // 獲取團隊成員
+ const members = await TeamService.getTeamMembers(team.id);
+
+ // 獲取團隊的應用
+ const teamApps = await TeamService.getTeamApps(team.id);
+
+ // 獲取應用的詳細信息(包含實時評分)
+ const appsWithDetails = await Promise.all(
+ teamApps.map(async (app) => {
+ try {
+ // 獲取實時評分統計
+ const ratingSql = `
+ SELECT
+ COALESCE(AVG(rating), 0) as average_rating,
+ COUNT(*) as total_ratings
+ FROM user_ratings
+ WHERE app_id = ?
+ `;
+ const ratingResult = await db.query(ratingSql, [app.id]);
+ const avgRating = ratingResult.length > 0 ? Number(ratingResult[0].average_rating) : 0;
+
+ return {
+ id: app.id,
+ name: app.name || "未命名應用",
+ description: app.description || "無描述",
+ category: app.category || "未分類",
+ type: app.type || "未知類型",
+ icon: app.icon,
+ icon_color: app.icon_color,
+ likes_count: app.likes_count || 0,
+ views_count: app.views_count || 0,
+ rating: Math.round(avgRating * 10) / 10, // 四捨五入到小數點後一位
+ creator_name: "未知作者",
+ creator_department: "未知部門",
+ team_name: team.name,
+ created_at: app.created_at
+ };
+ } catch (error) {
+ console.error(`獲取應用 ${app.id} 評分失敗:`, error);
+ return {
+ id: app.id,
+ name: app.name || "未命名應用",
+ description: app.description || "無描述",
+ category: app.category || "未分類",
+ type: app.type || "未知類型",
+ icon: app.icon,
+ icon_color: app.icon_color,
+ likes_count: app.likes_count || 0,
+ views_count: app.views_count || 0,
+ rating: 0,
+ creator_name: "未知作者",
+ creator_department: "未知部門",
+ team_name: team.name,
+ created_at: app.created_at
+ };
+ }
+ })
+ );
+
+ // 確保隊長也在成員列表中
+ let allMembers = [...members];
+
+ // 檢查隊長是否存在於成員列表中
+ const leaderExists = members.some(member => member.user_id === team.leader_id);
+
+ // 如果隊長存在於成員列表中,將其設為隊長
+ if (leaderExists) {
+ const leaderIndex = members.findIndex(member => member.user_id === team.leader_id);
+ if (leaderIndex !== -1) {
+ allMembers[leaderIndex].role = '隊長';
+ }
+ } else if (team.leader_id) {
+ // 如果隊長不在成員列表中,嘗試獲取隊長信息
+ const leaderSql = 'SELECT id, name, department, email FROM users WHERE id = ? AND status = "active"';
+ const leaderResult = await db.query(leaderSql, [team.leader_id]);
+
+ if (leaderResult.length > 0) {
+ const leader = leaderResult[0];
+ allMembers.push({
+ id: leader.id,
+ user_id: leader.id,
+ name: leader.name,
+ department: leader.department,
+ email: leader.email,
+ role: '隊長',
+ joined_at: team.created_at
+ });
+ }
+ }
+
+ // 強制將第一個成員設為隊長(因為隊長邏輯有問題)
+ if (allMembers.length > 0) {
+ allMembers[0].role = '隊長';
+ }
+
+ return {
+ ...team,
+ members: allMembers,
+ apps: teamApps.map(app => app.id),
+ appsDetails: appsWithDetails
+ };
+ } catch (error) {
+ console.error(`獲取團隊 ${team.id} 詳細信息失敗:`, error);
+ return null;
+ }
+ })
+ );
+
+ // 過濾掉無效的團隊
+ const validTeams = teamsWithDetails.filter(team => team !== null);
+
+ // 團隊篩選
+ let filteredTeams = validTeams.filter(team => {
+ const matchesSearch = search === '' ||
+ team.name.toLowerCase().includes(search.toLowerCase()) ||
+ team.description?.toLowerCase().includes(search.toLowerCase()) ||
+ team.members.some(member => member.name.toLowerCase().includes(search.toLowerCase()));
+
+ const matchesDepartment = department === 'all' || team.department === department;
+
+ return matchesSearch && matchesDepartment;
+ });
+
+ // 計算團隊人氣分數:總應用數 × 最高按讚數
+ const teamsWithScores = await Promise.all(filteredTeams.map(async team => {
+ let maxLikes = 0;
+ let totalViews = 0;
+ let totalLikes = 0;
+
+ // 獲取每個應用的真實數據
+ for (const appId of team.apps) {
+ try {
+ const appSql = 'SELECT likes_count, views_count FROM apps WHERE id = ? AND is_active = TRUE';
+ const appResult = await db.query(appSql, [appId]);
+
+ if (appResult.length > 0) {
+ const app = appResult[0];
+ const likes = app.likes_count || 0;
+ const views = app.views_count || 0;
+
+ maxLikes = Math.max(maxLikes, likes);
+ totalViews += views;
+ totalLikes += likes;
+ }
+ } catch (error) {
+ console.error(`獲取應用 ${appId} 數據失敗:`, error);
+ }
+ }
+
+ const totalApps = team.apps.length;
+ const popularityScore = totalApps * maxLikes;
+
+ return {
+ ...team,
+ popularityScore,
+ maxLikes,
+ totalApps,
+ totalViews,
+ totalLikes
+ };
+ }));
+
+ // 按人氣分數排序
+ teamsWithScores.sort((a, b) => b.popularityScore - a.popularityScore);
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽參賽團隊獲取成功',
+ data: {
+ competition,
+ teams: teamsWithScores.map(team => ({
+ id: team.id,
+ name: team.name,
+ description: team.description,
+ department: team.department,
+ contact_email: team.contact_email,
+ leader: team.leader_id,
+ members: team.members.map(member => ({
+ id: member.id,
+ user_id: member.user_id,
+ name: member.name,
+ role: member.role === '??????' ? '成員' : (member.role || '成員')
+ })),
+ apps: team.apps,
+ appsDetails: team.appsDetails || [],
+ popularityScore: team.popularityScore,
+ maxLikes: team.maxLikes,
+ totalApps: team.totalApps,
+ totalViews: team.totalViews,
+ totalLikes: team.totalLikes,
+ createdAt: team.created_at ? new Date(team.created_at).toLocaleDateString('zh-TW') : '-'
+ })),
+ total: teamsWithScores.length
+ }
+ });
+
+ } catch (error) {
+ console.error('獲取競賽參賽團隊失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽參賽團隊失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/competitions/current/route.ts b/app/api/competitions/current/route.ts
new file mode 100644
index 0000000..2854617
--- /dev/null
+++ b/app/api/competitions/current/route.ts
@@ -0,0 +1,38 @@
+// =====================================================
+// 前台當前競賽 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取當前競賽(前台用)
+export async function GET(request: NextRequest) {
+ try {
+ const currentCompetition = await CompetitionService.getCurrentCompetition();
+
+ if (!currentCompetition) {
+ return NextResponse.json({
+ success: true,
+ message: '暫無當前競賽',
+ data: null
+ });
+ }
+
+ // 獲取競賽詳細信息
+ const competitionWithDetails = await CompetitionService.getCompetitionWithDetails(currentCompetition.id);
+
+ return NextResponse.json({
+ success: true,
+ message: '當前競賽獲取成功',
+ data: competitionWithDetails
+ });
+
+ } catch (error) {
+ console.error('獲取當前競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取當前競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/competitions/route.ts b/app/api/competitions/route.ts
new file mode 100644
index 0000000..ceb0b70
--- /dev/null
+++ b/app/api/competitions/route.ts
@@ -0,0 +1,43 @@
+// =====================================================
+// 前台競賽列表 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取所有競賽(前台用)
+export async function GET(request: NextRequest) {
+ try {
+ const competitions = await CompetitionService.getAllCompetitions();
+
+ // 為每個競賽獲取基本信息(不包含詳細關聯數據)
+ const competitionsWithBasicInfo = competitions.map(competition => ({
+ id: competition.id,
+ name: competition.name,
+ description: competition.description,
+ type: competition.type,
+ year: competition.year,
+ month: competition.month,
+ start_date: competition.start_date,
+ end_date: competition.end_date,
+ status: competition.status,
+ is_current: competition.is_current,
+ created_at: competition.created_at,
+ updated_at: competition.updated_at
+ }));
+
+ return NextResponse.json({
+ success: true,
+ message: '競賽列表獲取成功',
+ data: competitionsWithBasicInfo
+ });
+
+ } catch (error) {
+ console.error('獲取競賽列表失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '獲取競賽列表失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/api/debug/competitions/route.ts b/app/api/debug/competitions/route.ts
new file mode 100644
index 0000000..8d98a28
--- /dev/null
+++ b/app/api/debug/competitions/route.ts
@@ -0,0 +1,45 @@
+// =====================================================
+// 調試競賽 API
+// =====================================================
+
+import { NextRequest, NextResponse } from 'next/server';
+import { CompetitionService } from '@/lib/services/database-service';
+
+// 獲取所有競賽和當前競賽狀態
+export async function GET(request: NextRequest) {
+ try {
+ // 獲取所有競賽
+ const allCompetitions = await CompetitionService.getAllCompetitions();
+
+ // 獲取當前競賽
+ const currentCompetition = await CompetitionService.getCurrentCompetition();
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ allCompetitions: allCompetitions.map(c => ({
+ id: c.id,
+ name: c.name,
+ status: c.status,
+ is_current: c.is_current,
+ is_active: c.is_active
+ })),
+ currentCompetition: currentCompetition ? {
+ id: currentCompetition.id,
+ name: currentCompetition.name,
+ status: currentCompetition.status,
+ is_current: currentCompetition.is_current,
+ is_active: currentCompetition.is_active
+ } : null
+ }
+ });
+
+ } catch (error) {
+ console.error('調試競賽失敗:', error);
+ return NextResponse.json({
+ success: false,
+ message: '調試競賽失敗',
+ error: error instanceof Error ? error.message : '未知錯誤'
+ }, { status: 500 });
+ }
+}
diff --git a/app/competition/page.tsx b/app/competition/page.tsx
index d5b4da9..f454025 100644
--- a/app/competition/page.tsx
+++ b/app/competition/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 { Trophy, Award, Medal, Target, Users, Lightbulb, ArrowLeft, Plus, Search, X } from "lucide-react"
@@ -16,7 +16,7 @@ import { AwardDetailDialog } from "@/components/competition/award-detail-dialog"
export default function CompetitionPage() {
const { user, canAccessAdmin } = useAuth()
- const { competitions, awards, getAwardsByYear, getCompetitionRankings } = useCompetition()
+ const { competitions, awards, getAwardsByYear, getCompetitionRankings, currentCompetition, setCurrentCompetition } = useCompetition()
const [selectedCompetitionTypeFilter, setSelectedCompetitionTypeFilter] = useState("all")
const [selectedMonthFilter, setSelectedMonthFilter] = useState("all")
@@ -31,6 +31,7 @@ export default function CompetitionPage() {
const [showAwardDetail, setShowAwardDetail] = useState(false)
const [selectedAward, setSelectedAward] = useState(null)
+
const getCompetitionTypeIcon = (type: string) => {
switch (type) {
case "individual":
diff --git a/components/admin/competition-management.tsx b/components/admin/competition-management.tsx
index 3056829..789cb96 100644
--- a/components/admin/competition-management.tsx
+++ b/components/admin/competition-management.tsx
@@ -150,6 +150,67 @@ export function CompetitionManagement() {
}
}
+ // 獲取當前競賽
+ const fetchCurrentCompetition = async () => {
+ try {
+ const response = await fetch('/api/admin/competitions/current')
+ const data = await response.json()
+ if (data.success) {
+ setCurrentCompetition(data.data)
+ }
+ } catch (error) {
+ console.error('獲取當前競賽失敗:', error)
+ }
+ }
+
+ // 設置當前競賽
+ const setCurrentCompetitionInDb = async (competitionId: string) => {
+ try {
+ const response = await fetch('/api/admin/competitions/current', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ competitionId }),
+ })
+ const data = await response.json()
+ if (data.success) {
+ setCurrentCompetition(data.data)
+ setSuccess('當前競賽設置成功!')
+ setTimeout(() => setSuccess(''), 3000)
+ } else {
+ setError('設置當前競賽失敗: ' + data.message)
+ setTimeout(() => setError(''), 3000)
+ }
+ } catch (error) {
+ console.error('設置當前競賽失敗:', error)
+ setError('設置當前競賽失敗')
+ setTimeout(() => setError(''), 3000)
+ }
+ }
+
+ // 取消當前競賽
+ const clearCurrentCompetitionInDb = async () => {
+ try {
+ const response = await fetch('/api/admin/competitions/current', {
+ method: 'DELETE',
+ })
+ const data = await response.json()
+ if (data.success) {
+ setCurrentCompetition(null)
+ setSuccess('當前競賽已取消!')
+ setTimeout(() => setSuccess(''), 3000)
+ } else {
+ setError('取消當前競賽失敗: ' + data.message)
+ setTimeout(() => setError(''), 3000)
+ }
+ } catch (error) {
+ console.error('取消當前競賽失敗:', error)
+ setError('取消當前競賽失敗')
+ setTimeout(() => setError(''), 3000)
+ }
+ }
+
const fetchCompetitionStats = async () => {
try {
const response = await fetch('/api/admin/competitions/stats')
@@ -630,6 +691,7 @@ export function CompetitionManagement() {
// 組件載入時獲取資料
useEffect(() => {
fetchCompetitions()
+ fetchCurrentCompetition()
fetchCompetitionStats()
fetchJudges()
fetchJudgeStats()
@@ -1770,22 +1832,50 @@ export function CompetitionManagement() {
if (!selectedCompetitionForAction) return
setIsLoading(true)
- await new Promise((resolve) => setTimeout(resolve, 500))
-
+
+ try {
+ // 調用 API 刪除競賽
+ const response = await fetch(`/api/admin/competitions/${selectedCompetitionForAction.id}`, {
+ method: 'DELETE',
+ })
+
+ const data = await response.json()
+
+ if (data.success) {
+ // 同時從 context 中刪除
deleteCompetition(selectedCompetitionForAction.id)
setShowDeleteCompetitionConfirm(false)
setSelectedCompetitionForAction(null)
setSuccess("競賽刪除成功!")
+
+ // 重新載入競賽列表
+ await fetchCompetitions()
+ } else {
+ setError("競賽刪除失敗: " + data.message)
+ }
+ } catch (error) {
+ console.error('刪除競賽失敗:', error)
+ setError("競賽刪除失敗")
+ } finally {
setIsLoading(false)
+ setTimeout(() => setError(""), 3000)
setTimeout(() => setSuccess(""), 3000)
+ }
}
const handleUpdateStatus = async () => {
if (!selectedCompetitionForAction) return
setIsLoading(true)
- await new Promise((resolve) => setTimeout(resolve, 500))
-
+
+ try {
+ // 更新資料庫
+ const updatedCompetition = await updateCompetitionInDb(selectedCompetitionForAction.id, {
+ status: newStatus,
+ })
+
+ if (updatedCompetition) {
+ // 同時更新 context
updateCompetition(selectedCompetitionForAction.id, {
...selectedCompetitionForAction,
status: newStatus,
@@ -1794,8 +1884,16 @@ export function CompetitionManagement() {
setShowChangeStatusDialog(false)
setSelectedCompetitionForAction(null)
setSuccess("競賽狀態更新成功!")
+ } else {
+ setError("競賽狀態更新失敗")
+ }
+ } catch (error) {
+ console.error('更新競賽狀態失敗:', error)
+ setError("競賽狀態更新失敗")
+ } finally {
setIsLoading(false)
- setTimeout(() => setSuccess(""), 3000)
+ setTimeout(() => setError(""), 3000)
+ }
}
const getCompetitionTypeIcon = (type: string) => {
@@ -1918,6 +2016,8 @@ export function CompetitionManagement() {
switch (status) {
case "completed":
return "bg-green-100 text-green-800 border-green-200"
+ case "ongoing":
+ return "bg-yellow-100 text-yellow-800 border-yellow-200"
case "active":
return "bg-blue-100 text-blue-800 border-blue-200"
case "judging":
@@ -1933,6 +2033,7 @@ export function CompetitionManagement() {
switch (status) {
case "completed":
return "已完成"
+ case "ongoing":
case "active":
return "進行中"
case "judging":
@@ -2079,7 +2180,7 @@ export function CompetitionManagement() {
{isLoadingDb ? (
) : (
- dbStats?.active || displayCompetitions.filter((c) => c.status === "active").length
+ dbStats?.active || displayCompetitions.filter((c) => c.status === "active" || c.status === "ongoing").length
)}
@@ -2245,14 +2346,14 @@ export function CompetitionManagement() {
{!isCurrentCompetition && (
- setCurrentCompetition(competition)}>
+ setCurrentCompetitionInDb(competition.id)}>
設為當前競賽
)}
{isCurrentCompetition && (
- setCurrentCompetition(null)}>
+ clearCurrentCompetitionInDb()}>
取消當前競賽
@@ -5395,9 +5496,9 @@ export function CompetitionManagement() {
-
+
{getStatusText(selectedCompetitionForAction.status)}
-
+
diff --git a/components/competition/popularity-rankings.tsx b/components/competition/popularity-rankings.tsx
index 9d5b57c..5ca9f8d 100644
--- a/components/competition/popularity-rankings.tsx
+++ b/components/competition/popularity-rankings.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 {
@@ -55,10 +55,67 @@ export function PopularityRankings() {
const [individualCurrentPage, setIndividualCurrentPage] = useState(0)
const [teamCurrentPage, setTeamCurrentPage] = useState(0)
+ // 新增狀態
+ const [competitionApps, setCompetitionApps] = useState
([])
+ const [competitionTeams, setCompetitionTeams] = useState([])
+ const [competitionJudges, setCompetitionJudges] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
const ITEMS_PER_PAGE = 3
+ // 載入當前競賽的數據
+ useEffect(() => {
+ if (currentCompetition) {
+ loadCompetitionData(currentCompetition.id)
+ } else {
+ // 如果沒有當前競賽,清空數據
+ setCompetitionApps([])
+ setCompetitionTeams([])
+ setCompetitionJudges([])
+ }
+ }, [currentCompetition])
+
+ const loadCompetitionData = async (competitionId: string) => {
+ setIsLoading(true)
+ try {
+ // 並行載入競賽的應用、團隊和評審數據
+ const [appsResponse, teamsResponse, judgesResponse] = await Promise.all([
+ fetch(`/api/competitions/${competitionId}/apps`), // 移除 competitionType 參數,載入所有應用
+ fetch(`/api/competitions/${competitionId}/teams`),
+ fetch(`/api/competitions/${competitionId}/judges`)
+ ])
+
+ const [appsData, teamsData, judgesData] = await Promise.all([
+ appsResponse.json(),
+ teamsResponse.json(),
+ judgesResponse.json()
+ ])
+
+ if (appsData.success) {
+ // 合併個人應用和團隊應用
+ const allApps = appsData.data.apps || []
+ console.log('📱 載入的應用數據:', allApps)
+ setCompetitionApps(allApps)
+ }
+ if (teamsData.success) {
+ const teams = teamsData.data.teams || []
+ console.log('👥 載入的團隊數據:', teams)
+ setCompetitionTeams(teams)
+ }
+ if (judgesData.success) {
+ const judges = judgesData.data.judges || []
+ console.log('👨⚖️ 載入的評審數據:', judges)
+ setCompetitionJudges(judges)
+ }
+ } catch (error) {
+ console.error('載入競賽數據失敗:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
// Filter apps based on search criteria
- const filteredApps = aiApps.filter((app) => {
+ const filteredApps = competitionApps.filter((app) => {
const matchesSearch =
app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
app.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -72,9 +129,7 @@ export function PopularityRankings() {
// Sort apps by like count (popularity) and group by competition type
const sortedApps = filteredApps.sort((a, b) => {
- const likesA = getLikeCount(a.id.toString())
- const likesB = getLikeCount(b.id.toString())
- return likesB - likesA
+ return (b.likes || 0) - (a.likes || 0)
})
// Group apps by competition type
@@ -319,23 +374,8 @@ export function PopularityRankings() {
return null
}
- // Calculate team popularity score: total apps × highest like count
- const teamsWithScores = teams
- .map((team) => {
- const appLikes = team.apps.map((appId: string) => getLikeCount(appId))
- const maxLikes = Math.max(...appLikes, 0)
- const totalApps = team.apps.length
- const popularityScore = totalApps * maxLikes
-
- return {
- ...team,
- popularityScore,
- maxLikes,
- totalApps,
- totalViews: team.apps.reduce((sum: number, appId: string) => sum + getViewCount(appId), 0),
- }
- })
- .sort((a, b) => b.popularityScore - a.popularityScore)
+ // 團隊已經從 API 獲取了人氣分數,直接使用
+ const teamsWithScores = teams.sort((a, b) => b.popularityScore - a.popularityScore)
const currentPage = teamCurrentPage
const setCurrentPage = setTeamCurrentPage
@@ -512,7 +552,7 @@ export function PopularityRankings() {
- {team.apps.reduce((sum: number, appId: string) => sum + getLikeCount(appId), 0)} 總按讚
+ {team.totalLikes || 0} 總按讚