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

316
app/tests/combined/page.tsx Normal file
View File

@@ -0,0 +1,316 @@
"use client"
import { useState, useEffect } from "react"
import { TestLayout } from "@/components/test-layout"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { Progress } from "@/components/ui/progress"
import { useRouter } from "next/navigation"
import { logicQuestions } from "@/lib/questions/logic-questions"
import { creativeQuestions } from "@/lib/questions/creative-questions"
import { calculateCombinedScore } from "@/lib/utils/score-calculator"
type TestPhase = "logic" | "creative" | "completed"
export default function CombinedTestPage() {
const router = useRouter()
const [phase, setPhase] = useState<TestPhase>("logic")
const [currentQuestion, setCurrentQuestion] = useState(0)
const [logicAnswers, setLogicAnswers] = useState<Record<number, string>>({})
const [creativeAnswers, setCreativeAnswers] = useState<Record<number, number>>({})
const [timeRemaining, setTimeRemaining] = useState(45 * 60) // 45 minutes total
// Timer effect
useEffect(() => {
const timer = setInterval(() => {
setTimeRemaining((prev) => {
if (prev <= 1) {
handleSubmit()
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [])
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
}
const getCurrentQuestions = () => {
return phase === "logic" ? logicQuestions : creativeQuestions
}
const getTotalQuestions = () => {
return logicQuestions.length + creativeQuestions.length
}
const getOverallProgress = () => {
const logicCompleted = phase === "logic" ? currentQuestion : logicQuestions.length
const creativeCompleted = phase === "creative" ? currentQuestion : 0
return logicCompleted + creativeCompleted
}
const handleAnswerChange = (value: string) => {
if (phase === "logic") {
setLogicAnswers((prev) => ({
...prev,
[currentQuestion]: value,
}))
} else {
setCreativeAnswers((prev) => ({
...prev,
[currentQuestion]: Number.parseInt(value),
}))
}
}
const handleNext = () => {
const currentQuestions = getCurrentQuestions()
if (currentQuestion < currentQuestions.length - 1) {
setCurrentQuestion((prev) => prev + 1)
} else if (phase === "logic") {
// Switch to creative phase
setPhase("creative")
setCurrentQuestion(0)
} else {
// Complete the test
handleSubmit()
}
}
const handlePrevious = () => {
if (currentQuestion > 0) {
setCurrentQuestion((prev) => prev - 1)
} else if (phase === "creative") {
// Go back to logic phase
setPhase("logic")
setCurrentQuestion(logicQuestions.length - 1)
}
}
const handleSubmit = () => {
// Calculate logic score
let logicCorrect = 0
logicQuestions.forEach((question, index) => {
if (logicAnswers[index] === question.correctAnswer) {
logicCorrect++
}
})
const logicScore = Math.round((logicCorrect / logicQuestions.length) * 100)
// Calculate creativity score
let creativityTotal = 0
creativeQuestions.forEach((question, index) => {
const answer = creativeAnswers[index] || 1
creativityTotal += question.isReverse ? 6 - answer : answer
})
const creativityMaxScore = creativeQuestions.length * 5
const creativityScore = Math.round((creativityTotal / creativityMaxScore) * 100)
// Calculate combined score
const combinedResult = calculateCombinedScore(logicScore, creativityScore)
// Store results
const results = {
type: "combined",
logicScore,
creativityScore,
overallScore: combinedResult.overallScore,
level: combinedResult.level,
description: combinedResult.description,
breakdown: combinedResult.breakdown,
logicAnswers,
creativeAnswers,
logicCorrect,
creativityTotal,
creativityMaxScore,
completedAt: new Date().toISOString(),
}
localStorage.setItem("combinedTestResults", JSON.stringify(results))
router.push("/results/combined")
}
const currentQuestions = getCurrentQuestions()
const currentQ = currentQuestions[currentQuestion]
const isLastQuestion = phase === "creative" && currentQuestion === creativeQuestions.length - 1
const hasAnswer =
phase === "logic" ? logicAnswers[currentQuestion] !== undefined : creativeAnswers[currentQuestion] !== undefined
const getPhaseTitle = () => {
if (phase === "logic") return "第一部分:邏輯思維測試"
return "第二部分:創意能力測試"
}
const getQuestionNumber = () => {
if (phase === "logic") return currentQuestion + 1
return logicQuestions.length + currentQuestion + 1
}
return (
<TestLayout
title="綜合能力測試"
currentQuestion={getQuestionNumber()}
totalQuestions={getTotalQuestions()}
timeRemaining={formatTime(timeRemaining)}
onBack={() => router.push("/")}
>
<div className="max-w-4xl mx-auto">
{/* Phase Indicator */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-foreground">{getPhaseTitle()}</h2>
<div className="text-sm text-muted-foreground">
{phase === "logic"
? `${currentQuestion + 1}/${logicQuestions.length}`
: `${currentQuestion + 1}/${creativeQuestions.length}`}
</div>
</div>
<Progress value={(getOverallProgress() / getTotalQuestions()) * 100} className="h-2" />
</div>
<Card className="mb-8">
<CardHeader>
<CardTitle className="text-xl text-balance">
{phase === "logic" ? currentQ.question : currentQ.statement}
</CardTitle>
{phase === "creative" && (
<p className="text-sm text-muted-foreground"></p>
)}
</CardHeader>
<CardContent>
<RadioGroup
value={
phase === "logic"
? logicAnswers[currentQuestion] || ""
: creativeAnswers[currentQuestion]?.toString() || ""
}
onValueChange={handleAnswerChange}
className="space-y-4"
>
{phase === "logic"
? // Logic question options
currentQ.options?.map((option: any, index: number) => (
<div
key={index}
className="flex items-center space-x-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
>
<RadioGroupItem value={option.value} id={`option-${index}`} />
<Label htmlFor={`option-${index}`} className="flex-1 cursor-pointer text-base leading-relaxed">
{option.text}
</Label>
</div>
))
: // Creative question options
[
{ value: "5", label: "我最符合", color: "text-green-600" },
{ value: "4", label: "比較符合", color: "text-green-500" },
{ value: "3", label: "一般", color: "text-yellow-500" },
{ value: "2", label: "不太符合", color: "text-orange-500" },
{ value: "1", label: "與我不符", color: "text-red-500" },
].map((option) => (
<div
key={option.value}
className="flex items-center space-x-4 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
>
<RadioGroupItem value={option.value} id={`option-${option.value}`} />
<Label
htmlFor={`option-${option.value}`}
className={`flex-1 cursor-pointer text-base font-medium ${option.color}`}
>
{option.label}
</Label>
<div className="text-sm text-muted-foreground font-mono">{option.value}</div>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
{/* Navigation */}
<div className="flex justify-between items-center">
<Button variant="outline" onClick={handlePrevious} disabled={phase === "logic" && currentQuestion === 0}>
</Button>
<div className="flex gap-2 max-w-md overflow-x-auto">
{/* Logic questions indicators */}
{logicQuestions.map((_, index) => (
<button
key={`logic-${index}`}
onClick={() => {
if (phase === "logic" || phase === "creative") {
setPhase("logic")
setCurrentQuestion(index)
}
}}
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
phase === "logic" && index === currentQuestion
? "bg-primary text-primary-foreground"
: logicAnswers[index] !== undefined
? "bg-primary/70 text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
{index + 1}
</button>
))}
{/* Separator */}
<div className="w-px h-8 bg-border mx-2"></div>
{/* Creative questions indicators */}
{creativeQuestions.map((_, index) => (
<button
key={`creative-${index}`}
onClick={() => {
if (phase === "creative") {
setCurrentQuestion(index)
}
}}
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
phase === "creative" && index === currentQuestion
? "bg-accent text-accent-foreground"
: creativeAnswers[index] !== undefined
? "bg-accent/70 text-accent-foreground"
: phase === "creative"
? "bg-muted text-muted-foreground hover:bg-muted/80"
: "bg-muted/50 text-muted-foreground/50"
}`}
disabled={phase === "logic"}
>
{logicQuestions.length + index + 1}
</button>
))}
</div>
{isLastQuestion ? (
<Button onClick={handleSubmit} disabled={!hasAnswer} className="bg-green-600 hover:bg-green-700">
</Button>
) : (
<Button onClick={handleNext} disabled={!hasAnswer}>
{phase === "logic" && currentQuestion === logicQuestions.length - 1 ? "進入第二部分" : "下一題"}
</Button>
)}
</div>
{/* Progress Summary */}
<div className="mt-8 text-center text-sm text-muted-foreground">
{getOverallProgress()} / {getTotalQuestions()}
<br />
{phase === "logic" ? "邏輯思維測試" : "創意能力測試"} (
{Object.keys(phase === "logic" ? logicAnswers : creativeAnswers).length} / {currentQuestions.length} )
</div>
</div>
</TestLayout>
)
}

177
app/tests/creative/page.tsx Normal file
View File

@@ -0,0 +1,177 @@
"use client"
import { useState, useEffect } from "react"
import { TestLayout } from "@/components/test-layout"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { useRouter } from "next/navigation"
import { creativeQuestions } from "@/lib/questions/creative-questions"
export default function CreativeTestPage() {
const router = useRouter()
const [currentQuestion, setCurrentQuestion] = useState(0)
const [answers, setAnswers] = useState<Record<number, number>>({})
const [timeRemaining, setTimeRemaining] = useState(30 * 60) // 30 minutes in seconds
// Timer effect
useEffect(() => {
const timer = setInterval(() => {
setTimeRemaining((prev) => {
if (prev <= 1) {
handleSubmit()
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [])
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
}
const handleAnswerChange = (value: string) => {
setAnswers((prev) => ({
...prev,
[currentQuestion]: Number.parseInt(value),
}))
}
const handleNext = () => {
if (currentQuestion < creativeQuestions.length - 1) {
setCurrentQuestion((prev) => prev + 1)
}
}
const handlePrevious = () => {
if (currentQuestion > 0) {
setCurrentQuestion((prev) => prev - 1)
}
}
const handleSubmit = () => {
// Calculate score based on creativity scoring
let totalScore = 0
creativeQuestions.forEach((question, index) => {
const answer = answers[index] || 1
// For creativity, higher scores indicate more creative thinking
totalScore += question.isReverse ? 6 - answer : answer
})
const maxScore = creativeQuestions.length * 5
const score = Math.round((totalScore / maxScore) * 100)
// Store results in localStorage
const results = {
type: "creative",
score,
totalScore,
maxScore,
answers,
completedAt: new Date().toISOString(),
}
localStorage.setItem("creativeTestResults", JSON.stringify(results))
router.push("/results/creative")
}
const currentQ = creativeQuestions[currentQuestion]
const isLastQuestion = currentQuestion === creativeQuestions.length - 1
const hasAnswer = answers[currentQuestion] !== undefined
const scaleOptions = [
{ value: "5", label: "我最符合", color: "text-green-600" },
{ value: "4", label: "比较符合", color: "text-green-500" },
{ value: "3", label: "一般", color: "text-yellow-500" },
{ value: "2", label: "不太符合", color: "text-orange-500" },
{ value: "1", label: "与我不符", color: "text-red-500" },
]
return (
<TestLayout
title="創意能力測試"
currentQuestion={currentQuestion + 1}
totalQuestions={creativeQuestions.length}
timeRemaining={formatTime(timeRemaining)}
onBack={() => router.push("/")}
>
<div className="max-w-4xl mx-auto">
<Card className="mb-8">
<CardHeader>
<CardTitle className="text-xl text-balance">{currentQ.statement}</CardTitle>
<p className="text-sm text-muted-foreground"></p>
</CardHeader>
<CardContent>
<RadioGroup
value={answers[currentQuestion]?.toString() || ""}
onValueChange={handleAnswerChange}
className="space-y-4"
>
{scaleOptions.map((option) => (
<div
key={option.value}
className="flex items-center space-x-4 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
>
<RadioGroupItem value={option.value} id={`option-${option.value}`} />
<Label
htmlFor={`option-${option.value}`}
className={`flex-1 cursor-pointer text-base font-medium ${option.color}`}
>
{option.label}
</Label>
<div className="text-sm text-muted-foreground font-mono">{option.value}</div>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
{/* Navigation */}
<div className="flex justify-between items-center">
<Button variant="outline" onClick={handlePrevious} disabled={currentQuestion === 0}>
</Button>
<div className="flex gap-2 max-w-md overflow-x-auto">
{creativeQuestions.map((_, index) => (
<button
key={index}
onClick={() => setCurrentQuestion(index)}
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
index === currentQuestion
? "bg-accent text-accent-foreground"
: answers[index] !== undefined
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
{index + 1}
</button>
))}
</div>
{isLastQuestion ? (
<Button onClick={handleSubmit} disabled={!hasAnswer} className="bg-green-600 hover:bg-green-700">
</Button>
) : (
<Button onClick={handleNext} disabled={!hasAnswer}>
</Button>
)}
</div>
{/* Progress Summary */}
<div className="mt-8 text-center text-sm text-muted-foreground">
{Object.keys(answers).length} / {creativeQuestions.length}
</div>
</div>
</TestLayout>
)
}

166
app/tests/logic/page.tsx Normal file
View File

@@ -0,0 +1,166 @@
"use client"
import { useState, useEffect } from "react"
import { TestLayout } from "@/components/test-layout"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { useRouter } from "next/navigation"
import { logicQuestions } from "@/lib/questions/logic-questions"
export default function LogicTestPage() {
const router = useRouter()
const [currentQuestion, setCurrentQuestion] = useState(0)
const [answers, setAnswers] = useState<Record<number, string>>({})
const [timeRemaining, setTimeRemaining] = useState(20 * 60) // 20 minutes in seconds
// Timer effect
useEffect(() => {
const timer = setInterval(() => {
setTimeRemaining((prev) => {
if (prev <= 1) {
handleSubmit()
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [])
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
}
const handleAnswerChange = (value: string) => {
setAnswers((prev) => ({
...prev,
[currentQuestion]: value,
}))
}
const handleNext = () => {
if (currentQuestion < logicQuestions.length - 1) {
setCurrentQuestion((prev) => prev + 1)
}
}
const handlePrevious = () => {
if (currentQuestion > 0) {
setCurrentQuestion((prev) => prev - 1)
}
}
const handleSubmit = () => {
// Calculate score
let correctAnswers = 0
logicQuestions.forEach((question, index) => {
if (answers[index] === question.correctAnswer) {
correctAnswers++
}
})
const score = Math.round((correctAnswers / logicQuestions.length) * 100)
// Store results in localStorage
const results = {
type: "logic",
score,
correctAnswers,
totalQuestions: logicQuestions.length,
answers,
completedAt: new Date().toISOString(),
}
localStorage.setItem("logicTestResults", JSON.stringify(results))
router.push("/results/logic")
}
const currentQ = logicQuestions[currentQuestion]
const isLastQuestion = currentQuestion === logicQuestions.length - 1
const hasAnswer = answers[currentQuestion] !== undefined
return (
<TestLayout
title="邏輯思維測試"
currentQuestion={currentQuestion + 1}
totalQuestions={logicQuestions.length}
timeRemaining={formatTime(timeRemaining)}
onBack={() => router.push("/")}
>
<div className="max-w-4xl mx-auto">
<Card className="mb-8">
<CardHeader>
<CardTitle className="text-xl text-balance">{currentQ.question}</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup value={answers[currentQuestion] || ""} onValueChange={handleAnswerChange} className="space-y-4">
{currentQ.options.map((option, index) => (
<div
key={index}
className="flex items-center space-x-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors"
>
<RadioGroupItem value={option.value} id={`option-${index}`} />
<Label htmlFor={`option-${index}`} className="flex-1 cursor-pointer text-base leading-relaxed">
{option.text}
</Label>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
{/* Navigation */}
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<Button variant="outline" onClick={handlePrevious} disabled={currentQuestion === 0} size="sm">
</Button>
{isLastQuestion ? (
<Button
onClick={handleSubmit}
disabled={!hasAnswer}
className="bg-green-600 hover:bg-green-700"
size="sm"
>
</Button>
) : (
<Button onClick={handleNext} disabled={!hasAnswer} size="sm">
</Button>
)}
</div>
<div className="flex flex-wrap justify-center gap-2 px-2">
{logicQuestions.map((_, index) => (
<button
key={index}
onClick={() => setCurrentQuestion(index)}
className={`w-8 h-8 rounded-full text-sm font-medium transition-colors flex-shrink-0 ${
index === currentQuestion
? "bg-primary text-primary-foreground"
: answers[index] !== undefined
? "bg-accent text-accent-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
{index + 1}
</button>
))}
</div>
</div>
{/* Progress Summary */}
<div className="mt-8 text-center text-sm text-muted-foreground">
{Object.keys(answers).length} / {logicQuestions.length}
</div>
</div>
</TestLayout>
)
}

151
app/tests/page.tsx Normal file
View File

@@ -0,0 +1,151 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Brain, Lightbulb, BarChart3, ArrowLeft } from "lucide-react"
import Link from "next/link"
export default function TestsPage() {
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="/">
<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-12">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-foreground mb-4"></h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Logic Test */}
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-primary/20">
<CardHeader className="text-center pb-4">
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-primary/20 transition-colors">
<Brain className="w-8 h-8 text-primary" />
</div>
<CardTitle className="text-2xl"></CardTitle>
<CardDescription className="text-base"></CardDescription>
</CardHeader>
<CardContent className="text-center">
<div className="space-y-3 mb-6">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">10 </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium"></span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">15-20 </span>
</div>
</div>
<Button asChild className="w-full">
<Link href="/tests/logic"></Link>
</Button>
</CardContent>
</Card>
{/* Creative Test */}
<Card className="group hover:shadow-lg transition-all duration-300 border-2 hover:border-accent/20">
<CardHeader className="text-center pb-4">
<div className="w-16 h-16 bg-accent/10 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-accent/20 transition-colors">
<Lightbulb className="w-8 h-8 text-accent" />
</div>
<CardTitle className="text-2xl"></CardTitle>
<CardDescription className="text-base"></CardDescription>
</CardHeader>
<CardContent className="text-center">
<div className="space-y-3 mb-6">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">20 </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">5</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">25-30 </span>
</div>
</div>
<Button
asChild
variant="outline"
className="w-full border-accent text-accent hover:bg-accent hover:text-accent-foreground bg-transparent"
>
<Link href="/tests/creative"></Link>
</Button>
</CardContent>
</Card>
{/* Combined Test */}
<Card className="group hover:shadow-lg transition-all duration-300 border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-accent/5 lg:col-span-1">
<CardHeader className="text-center pb-4">
<div className="w-16 h-16 bg-gradient-to-br from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
<BarChart3 className="w-8 h-8 text-white" />
</div>
<CardTitle className="text-2xl"></CardTitle>
<CardDescription className="text-base"> + </CardDescription>
</CardHeader>
<CardContent className="text-center">
<div className="space-y-3 mb-6">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">30 </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium"> + </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">40-45 </span>
</div>
</div>
<Button asChild size="lg" className="w-full text-lg">
<Link href="/tests/combined"></Link>
</Button>
</CardContent>
</Card>
</div>
{/* Additional Info */}
<div className="mt-12 text-center">
<Card className="max-w-2xl mx-auto">
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-3"></h3>
<div className="text-sm text-muted-foreground space-y-2 text-left">
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
)
}