Initial commit

This commit is contained in:
2025-09-25 12:30:25 +08:00
commit 2765d9df54
100 changed files with 16023 additions and 0 deletions

View File

@@ -0,0 +1,354 @@
"use client"
import type React from "react"
import { useState } from "react"
import { ProtectedRoute } from "@/components/protected-route"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
ArrowLeft,
Upload,
Download,
Brain,
Lightbulb,
FileSpreadsheet,
CheckCircle,
AlertCircle,
Info,
} from "lucide-react"
import Link from "next/link"
import { logicQuestions } from "@/lib/questions/logic-questions"
import { creativeQuestions } from "@/lib/questions/creative-questions"
import { parseExcelFile, type ImportResult } from "@/lib/utils/excel-parser"
export default function QuestionsManagementPage() {
return (
<ProtectedRoute adminOnly>
<QuestionsManagementContent />
</ProtectedRoute>
)
}
function QuestionsManagementContent() {
const [activeTab, setActiveTab] = useState("logic")
const [isImporting, setIsImporting] = useState(false)
const [importResult, setImportResult] = useState<ImportResult | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [importType, setImportType] = useState<"logic" | "creative">("logic")
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
if (
file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
file.type === "application/vnd.ms-excel" ||
file.name.endsWith(".xlsx") ||
file.name.endsWith(".xls")
) {
setSelectedFile(file)
setImportResult(null)
} else {
setImportResult({
success: false,
message: "請選擇 Excel 檔案 (.xlsx 或 .xls)",
})
}
}
}
const handleImport = async () => {
if (!selectedFile) {
setImportResult({
success: false,
message: "請先選擇要匯入的 Excel 檔案",
})
return
}
setIsImporting(true)
try {
const result = await parseExcelFile(selectedFile, importType)
setImportResult(result)
if (result.success) {
setSelectedFile(null)
// 重置檔案輸入
const fileInput = document.getElementById("file-input") as HTMLInputElement
if (fileInput) fileInput.value = ""
}
} catch (error) {
setImportResult({
success: false,
message: "匯入失敗,請檢查檔案格式是否正確",
errors: [error instanceof Error ? error.message : "未知錯誤"],
})
} finally {
setIsImporting(false)
}
}
const downloadTemplate = (type: "logic" | "creative") => {
let csvContent = ""
if (type === "logic") {
csvContent = [
["題目ID", "題目內容", "選項A", "選項B", "選項C", "選項D", "正確答案", "解釋"],
[
"1",
"範例題目如果所有A都是B所有B都是C那麼",
"所有A都是C",
"所有C都是A",
"有些A不是C",
"無法確定",
"A",
"根據邏輯推理...",
],
["2", "在序列 2, 4, 8, 16, ? 中,下一個數字是?", "24", "32", "30", "28", "B", "每個數字都是前一個數字的2倍"],
]
.map((row) => row.join(","))
.join("\n")
} else {
csvContent = [
["題目ID", "陳述內容", "類別", "是否反向計分"],
["1", "我經常能想出創新的解決方案", "innovation", "否"],
["2", "我更喜歡按照既定規則工作", "flexibility", "是"],
["3", "我喜歡嘗試新的做事方法", "innovation", "否"],
]
.map((row) => row.join(","))
.join("\n")
}
const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" })
const link = document.createElement("a")
const url = URL.createObjectURL(blob)
link.setAttribute("href", url)
link.setAttribute("download", `${type === "logic" ? "邏輯思維" : "創意能力"}題目範本.csv`)
link.style.visibility = "hidden"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card/50 backdrop-blur-sm">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link href="/dashboard">
<ArrowLeft className="w-4 h-4 mr-2" />
</Link>
</Button>
<div>
<h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto space-y-8">
{/* Import Section */}
<Card className="border-2 border-primary/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5 text-primary" />
Excel
</CardTitle>
<CardDescription> Excel </CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* File Upload */}
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="file-input"> Excel </Label>
<Input
id="file-input"
type="file"
accept=".xlsx,.xls"
onChange={handleFileSelect}
className="cursor-pointer"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={importType} onValueChange={(value: "logic" | "creative") => setImportType(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="logic"></SelectItem>
<SelectItem value="creative"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
{selectedFile && (
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
<FileSpreadsheet className="w-4 h-4 text-blue-600" />
<span className="text-sm text-blue-800">{selectedFile.name}</span>
</div>
)}
<div className="flex gap-3">
<Button onClick={handleImport} disabled={!selectedFile || isImporting} className="flex-1">
{isImporting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
...
</>
) : (
<>
<Upload className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</div>
{/* Import Result */}
{importResult && (
<Alert variant={importResult.success ? "default" : "destructive"}>
{importResult.success ? <CheckCircle className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
<AlertDescription>
{importResult.message}
{importResult.errors && (
<ul className="mt-2 list-disc list-inside">
{importResult.errors.map((error, index) => (
<li key={index} className="text-sm">
{error}
</li>
))}
</ul>
)}
</AlertDescription>
</Alert>
)}
{/* Template Download */}
<div className="border-t pt-4">
<div className="flex items-center gap-2 mb-3">
<Info className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={() => downloadTemplate("logic")} className="flex-1">
<Download className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => downloadTemplate("creative")} className="flex-1">
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Current Questions */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="logic" className="flex items-center gap-2">
<Brain className="w-4 h-4" />
({logicQuestions.length})
</TabsTrigger>
<TabsTrigger value="creative" className="flex items-center gap-2">
<Lightbulb className="w-4 h-4" />
({creativeQuestions.length})
</TabsTrigger>
</TabsList>
<TabsContent value="logic" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"></h3>
<Badge variant="outline">{logicQuestions.length} </Badge>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logicQuestions.slice(0, 10).map((question) => (
<TableRow key={question.id}>
<TableCell className="font-medium">{question.id}</TableCell>
<TableCell className="max-w-md truncate">{question.question}</TableCell>
<TableCell>{question.options.length}</TableCell>
<TableCell>
<Badge className="bg-green-500 text-white">{question.correctAnswer}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
<TabsContent value="creative" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold"></h3>
<Badge variant="outline">{creativeQuestions.length} </Badge>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{creativeQuestions.slice(0, 10).map((question) => (
<TableRow key={question.id}>
<TableCell className="font-medium">{question.id}</TableCell>
<TableCell className="max-w-md truncate">{question.statement}</TableCell>
<TableCell>
<Badge variant="secondary">{question.category}</Badge>
</TableCell>
<TableCell>
{question.isReverse ? (
<Badge className="bg-orange-500 text-white"></Badge>
) : (
<Badge variant="outline"></Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
</div>
</div>
)
}