新增資料庫、用戶註冊、登入的功能
This commit is contained in:
@@ -1,195 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { CheckCircle, Edit, Loader2 } from "lucide-react"
|
||||
|
||||
export default function ScoringFormTestPage() {
|
||||
const [showScoringForm, setShowScoringForm] = useState(false)
|
||||
const [manualScoring, setManualScoring] = useState({
|
||||
judgeId: "judge1",
|
||||
participantId: "app1",
|
||||
scores: {
|
||||
"創新性": 0,
|
||||
"技術性": 0,
|
||||
"實用性": 0,
|
||||
"展示效果": 0,
|
||||
"影響力": 0
|
||||
},
|
||||
comments: ""
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const scoringRules = [
|
||||
{ name: "創新性", description: "技術創新程度和獨特性", weight: 25 },
|
||||
{ name: "技術性", description: "技術實現的複雜度和穩定性", weight: 20 },
|
||||
{ name: "實用性", description: "實際應用價值和用戶體驗", weight: 25 },
|
||||
{ name: "展示效果", description: "演示效果和表達能力", weight: 15 },
|
||||
{ name: "影響力", description: "對行業和社會的潛在影響", weight: 15 }
|
||||
]
|
||||
|
||||
const calculateTotalScore = (scores: Record<string, number>): number => {
|
||||
let totalScore = 0
|
||||
let totalWeight = 0
|
||||
|
||||
scoringRules.forEach(rule => {
|
||||
const score = scores[rule.name] || 0
|
||||
const weight = rule.weight || 1
|
||||
totalScore += score * weight
|
||||
totalWeight += weight
|
||||
})
|
||||
|
||||
return totalWeight > 0 ? Math.round(totalScore / totalWeight) : 0
|
||||
}
|
||||
|
||||
const handleSubmitScore = async () => {
|
||||
setIsLoading(true)
|
||||
// 模擬提交
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setShowScoringForm(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">評分表單測試</h1>
|
||||
<p className="text-gray-600">測試完整的評分表單功能</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>評分表單演示</CardTitle>
|
||||
<CardDescription>點擊按鈕查看完整的評分表單</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={() => setShowScoringForm(true)} size="lg">
|
||||
<Edit className="w-5 h-5 mr-2" />
|
||||
開啟評分表單
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showScoringForm} onOpenChange={setShowScoringForm}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center space-x-2">
|
||||
<Edit className="w-5 h-5" />
|
||||
<span>評分表單</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
為參賽者進行評分,請根據各項指標進行評分
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 評分項目 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">評分項目</h3>
|
||||
{scoringRules.map((rule, index) => (
|
||||
<div key={index} className="space-y-4 p-6 border rounded-lg bg-white shadow-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<Label className="text-lg font-semibold text-gray-900">{rule.name}</Label>
|
||||
<p className="text-sm text-gray-600 mt-2 leading-relaxed">{rule.description}</p>
|
||||
<p className="text-xs text-purple-600 mt-2 font-medium">權重:{rule.weight}%</p>
|
||||
</div>
|
||||
<div className="text-right ml-4">
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{manualScoring.scores[rule.name] || 0} / 10
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 評分按鈕 */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Array.from({ length: 10 }, (_, i) => i + 1).map((score) => (
|
||||
<button
|
||||
key={score}
|
||||
type="button"
|
||||
onClick={() => setManualScoring({
|
||||
...manualScoring,
|
||||
scores: { ...manualScoring.scores, [rule.name]: score }
|
||||
})}
|
||||
className={`w-12 h-12 rounded-lg border-2 font-semibold text-lg transition-all duration-200 ${
|
||||
(manualScoring.scores[rule.name] || 0) === score
|
||||
? 'bg-blue-600 text-white border-blue-600 shadow-lg scale-105'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-400 hover:bg-blue-50 hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
{score}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 總分顯示 */}
|
||||
<div className="p-6 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border-2 border-blue-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<span className="text-xl font-bold text-gray-900">總分</span>
|
||||
<p className="text-sm text-gray-600 mt-1">根據權重計算的綜合評分</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-4xl font-bold text-blue-600">
|
||||
{calculateTotalScore(manualScoring.scores)}
|
||||
</span>
|
||||
<span className="text-xl text-gray-500 font-medium">/ 10</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 評審意見 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-lg font-semibold">評審意見 *</Label>
|
||||
<Textarea
|
||||
placeholder="請詳細填寫評審意見、優點分析、改進建議等..."
|
||||
value={manualScoring.comments}
|
||||
onChange={(e) => setManualScoring({ ...manualScoring, comments: e.target.value })}
|
||||
rows={6}
|
||||
className="min-h-[120px] resize-none"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">請提供具體的評審意見,包括項目的優點、不足之處和改進建議</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-4 pt-6 border-t border-gray-200">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => setShowScoringForm(false)}
|
||||
className="px-8"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitScore}
|
||||
disabled={isLoading}
|
||||
size="lg"
|
||||
className="px-8 bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||
提交中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
提交評分
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
import { ScoringManagement } from "@/components/admin/scoring-management"
|
||||
|
||||
export default function ScoringTestPage() {
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold">評分管理測試</h1>
|
||||
<p className="text-gray-600">測試動態評分項目功能</p>
|
||||
</div>
|
||||
<ScoringManagement />
|
||||
</div>
|
||||
)
|
||||
}
|
115
app/api/auth/login/route.ts
Normal file
115
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/database';
|
||||
import { generateToken, validatePassword, comparePassword } from '@/lib/auth';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, password } = body;
|
||||
|
||||
// 驗證輸入
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: '請提供電子郵件和密碼' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證密碼格式
|
||||
const passwordValidation = await validatePassword(password);
|
||||
if (!passwordValidation.isValid) {
|
||||
return NextResponse.json(
|
||||
{ error: '密碼格式不正確', details: passwordValidation.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 查詢用戶
|
||||
const user = await db.queryOne<{
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
avatar?: string;
|
||||
department: string;
|
||||
role: 'user' | 'developer' | 'admin';
|
||||
join_date: string;
|
||||
total_likes: number;
|
||||
total_views: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>(
|
||||
'SELECT * FROM users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
logger.logAuth('login', email, false, request.ip || 'unknown');
|
||||
return NextResponse.json(
|
||||
{ error: '電子郵件或密碼不正確' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證密碼
|
||||
const isPasswordValid = await comparePassword(password, user.password_hash);
|
||||
if (!isPasswordValid) {
|
||||
logger.logAuth('login', email, false, request.ip || 'unknown');
|
||||
return NextResponse.json(
|
||||
{ error: '電子郵件或密碼不正確' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// 生成 JWT Token
|
||||
const token = generateToken({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
|
||||
// 更新最後登入時間
|
||||
await db.update(
|
||||
'users',
|
||||
{ updated_at: new Date().toISOString().slice(0, 19).replace('T', ' ') },
|
||||
{ id: user.id }
|
||||
);
|
||||
|
||||
// 記錄成功登入
|
||||
logger.logAuth('login', email, true, request.ip || 'unknown');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.logRequest('POST', '/api/auth/login', 200, duration, user.id);
|
||||
|
||||
return NextResponse.json({
|
||||
message: '登入成功',
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
avatar: user.avatar,
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
joinDate: user.join_date,
|
||||
totalLikes: user.total_likes,
|
||||
totalViews: user.total_views
|
||||
},
|
||||
token,
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.logError(error as Error, 'Login API');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.logRequest('POST', '/api/auth/login', 500, duration);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: '內部伺服器錯誤' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
47
app/api/auth/me/route.ts
Normal file
47
app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { authenticateUser } from '@/lib/auth';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 驗證用戶
|
||||
const user = await authenticateUser(request);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: '未授權訪問' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.logRequest('GET', '/api/auth/me', 200, duration, user.id);
|
||||
|
||||
return NextResponse.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
avatar: user.avatar,
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
joinDate: user.joinDate,
|
||||
totalLikes: user.totalLikes,
|
||||
totalViews: user.totalViews
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.logError(error as Error, 'Get Current User API');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.logRequest('GET', '/api/auth/me', 500, duration);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: '內部伺服器錯誤' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
113
app/api/auth/register/route.ts
Normal file
113
app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, generateId } from '@/lib/database';
|
||||
import { validateUserData, validatePassword, hashPassword } from '@/lib/auth';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log('開始處理註冊請求...');
|
||||
|
||||
const body = await request.json();
|
||||
console.log('請求體:', body);
|
||||
|
||||
const { name, email, password, department, role = 'user' } = body;
|
||||
|
||||
// 驗證用戶資料
|
||||
console.log('驗證用戶資料...');
|
||||
const userValidation = validateUserData({ name, email, department, role });
|
||||
if (!userValidation.isValid) {
|
||||
console.log('用戶資料驗證失敗:', userValidation.errors);
|
||||
return NextResponse.json(
|
||||
{ error: '用戶資料驗證失敗', details: userValidation.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 驗證密碼
|
||||
console.log('驗證密碼...');
|
||||
const passwordValidation = await validatePassword(password);
|
||||
if (!passwordValidation.isValid) {
|
||||
console.log('密碼驗證失敗:', passwordValidation.errors);
|
||||
return NextResponse.json(
|
||||
{ error: '密碼格式不正確', details: passwordValidation.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 檢查電子郵件是否已存在
|
||||
console.log('檢查電子郵件是否已存在...');
|
||||
const existingUser = await db.queryOne(
|
||||
'SELECT id FROM users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (existingUser) {
|
||||
console.log('電子郵件已存在');
|
||||
return NextResponse.json(
|
||||
{ error: '此電子郵件地址已被註冊' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// 加密密碼
|
||||
console.log('加密密碼...');
|
||||
const passwordHash = await hashPassword(password);
|
||||
console.log('密碼加密完成');
|
||||
|
||||
// 準備用戶資料
|
||||
console.log('準備用戶資料...');
|
||||
const userId = generateId();
|
||||
const userData = {
|
||||
id: userId,
|
||||
name: name.trim(),
|
||||
email: email.toLowerCase().trim(),
|
||||
password_hash: passwordHash,
|
||||
department: department.trim(),
|
||||
role,
|
||||
join_date: new Date().toISOString().split('T')[0],
|
||||
total_likes: 0,
|
||||
total_views: 0,
|
||||
created_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||||
updated_at: new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
};
|
||||
|
||||
console.log('插入用戶資料...');
|
||||
// 插入用戶資料
|
||||
await db.insert('users', userData);
|
||||
console.log('用戶資料插入成功');
|
||||
|
||||
// 記錄註冊成功
|
||||
logger.logAuth('register', email, true, 'unknown');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.logRequest('POST', '/api/auth/register', 201, duration, userId);
|
||||
|
||||
return NextResponse.json({
|
||||
message: '註冊成功',
|
||||
user: {
|
||||
id: userData.id,
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
department: userData.department,
|
||||
role: userData.role,
|
||||
joinDate: userData.join_date,
|
||||
totalLikes: userData.total_likes,
|
||||
totalViews: userData.total_views
|
||||
}
|
||||
}, { status: 201 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('註冊 API 錯誤:', error);
|
||||
logger.logError(error as Error, 'Register API');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.logRequest('POST', '/api/auth/register', 500, duration);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
19
app/api/auth/reset-password/confirm/route.ts
Normal file
19
app/api/auth/reset-password/confirm/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/database';
|
||||
import { hashPassword } from '@/lib/auth';
|
||||
import { codeMap } from '../request/route';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, code, newPassword } = await request.json();
|
||||
if (!email || !code || !newPassword) return NextResponse.json({ error: '缺少參數' }, { status: 400 });
|
||||
const validCode = codeMap.get(email);
|
||||
if (!validCode || validCode !== code) return NextResponse.json({ error: '驗證碼錯誤' }, { status: 400 });
|
||||
const passwordHash = await hashPassword(newPassword);
|
||||
await db.update('users', { password_hash: passwordHash }, { email });
|
||||
codeMap.delete(email);
|
||||
return NextResponse.json({ message: '密碼重設成功' });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
20
app/api/auth/reset-password/request/route.ts
Normal file
20
app/api/auth/reset-password/request/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/database';
|
||||
|
||||
const codeMap = new Map();
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email } = await request.json();
|
||||
if (!email) return NextResponse.json({ error: '請提供 email' }, { status: 400 });
|
||||
const user = await db.queryOne('SELECT id FROM users WHERE email = ?', [email]);
|
||||
if (!user) return NextResponse.json({ error: '用戶不存在' }, { status: 404 });
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
codeMap.set(email, code);
|
||||
// 實際應發送 email,這裡直接回傳
|
||||
return NextResponse.json({ message: '驗證碼已產生', code });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
export { codeMap };
|
35
app/api/route.ts
Normal file
35
app/api/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/database';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 健康檢查
|
||||
const isHealthy = await db.healthCheck();
|
||||
|
||||
if (!isHealthy) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Database connection failed' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
// 獲取基本統計
|
||||
const stats = await db.getDatabaseStats();
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'AI Platform API is running',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
database: {
|
||||
status: 'connected',
|
||||
stats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('API Health Check Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
47
app/api/users/route.ts
Normal file
47
app/api/users/route.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdmin } from '@/lib/auth';
|
||||
import { db } from '@/lib/database';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// 驗證管理員權限
|
||||
const admin = await requireAdmin(request);
|
||||
// 查詢參數
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
|
||||
const limit = Math.max(1, Math.min(100, parseInt(searchParams.get('limit') || '20', 10)));
|
||||
const offset = (page - 1) * limit;
|
||||
// 查詢用戶總數
|
||||
const countResult = await db.queryOne<{ total: number }>('SELECT COUNT(*) as total FROM users');
|
||||
const total = countResult?.total || 0;
|
||||
// 查詢用戶列表
|
||||
const users = await db.query(
|
||||
`SELECT id, name, email, avatar, department, role, join_date, total_likes, total_views, created_at, updated_at FROM users ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`
|
||||
);
|
||||
// 分頁資訊
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
const hasNext = page < totalPages;
|
||||
const hasPrev = page > 1;
|
||||
return NextResponse.json({
|
||||
users: users.map(user => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
avatar: user.avatar,
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
joinDate: user.join_date,
|
||||
totalLikes: user.total_likes,
|
||||
totalViews: user.total_views,
|
||||
createdAt: user.created_at,
|
||||
updatedAt: user.updated_at
|
||||
})),
|
||||
pagination: { page, limit, total, totalPages, hasNext, hasPrev }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.logError(error as Error, 'Users List API');
|
||||
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
23
app/api/users/stats/route.ts
Normal file
23
app/api/users/stats/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdmin } from '@/lib/auth';
|
||||
import { db } from '@/lib/database';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
await requireAdmin(request);
|
||||
const total = await db.queryOne<{ count: number }>('SELECT COUNT(*) as count FROM users');
|
||||
const admin = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'admin'");
|
||||
const developer = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'developer'");
|
||||
const user = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE role = 'user'");
|
||||
const today = await db.queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE join_date = CURDATE()");
|
||||
return NextResponse.json({
|
||||
total: total?.count || 0,
|
||||
admin: admin?.count || 0,
|
||||
developer: developer?.count || 0,
|
||||
user: user?.count || 0,
|
||||
today: today?.count || 0
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: '內部伺服器錯誤', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
@@ -135,7 +135,7 @@ export default function CompetitionPage() {
|
||||
const filteredAwards = getFilteredAwards()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -165,9 +165,9 @@ export default function CompetitionPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">AI 創新競賽</h2>
|
||||
|
@@ -344,7 +344,7 @@ export default function AIShowcasePlatform() {
|
||||
const filteredAwards = getFilteredAwards()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -401,9 +401,9 @@ export default function AIShowcasePlatform() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{showCompetition ? (
|
||||
// Competition Content
|
||||
<>
|
||||
@@ -999,7 +999,7 @@ export default function AIShowcasePlatform() {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||
<footer className="bg-white border-t border-gray-200 mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center">
|
||||
<div className="text-sm text-gray-500 mb-4 md:mb-0">
|
||||
|
Reference in New Issue
Block a user