358 lines
14 KiB
TypeScript
358 lines
14 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import Link from 'next/link'
|
||
|
||
export default function LearnPage() {
|
||
const [currentCardIndex, setCurrentCardIndex] = useState(0)
|
||
const [isFlipped, setIsFlipped] = useState(false)
|
||
const [mode, setMode] = useState<'flip' | 'quiz'>('flip')
|
||
const [score, setScore] = useState({ correct: 0, total: 0 })
|
||
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
|
||
const [showResult, setShowResult] = useState(false)
|
||
|
||
// Mock data
|
||
const cards = [
|
||
{
|
||
id: 1,
|
||
word: 'negotiate',
|
||
partOfSpeech: 'verb',
|
||
pronunciation: '/nɪˈɡoʊʃieɪt/',
|
||
translation: '協商、談判',
|
||
definition: 'To discuss something with someone in order to reach an agreement',
|
||
example: 'We need to negotiate a better deal with our suppliers.',
|
||
exampleTranslation: '我們需要與供應商協商更好的交易。',
|
||
synonyms: ['bargain', 'discuss', 'mediate'],
|
||
difficulty: 'intermediate'
|
||
},
|
||
{
|
||
id: 2,
|
||
word: 'perspective',
|
||
partOfSpeech: 'noun',
|
||
pronunciation: '/pərˈspektɪv/',
|
||
translation: '觀點、看法',
|
||
definition: 'A particular way of considering something',
|
||
example: 'From my perspective, this is the best solution.',
|
||
exampleTranslation: '從我的角度來看,這是最好的解決方案。',
|
||
synonyms: ['viewpoint', 'outlook', 'stance'],
|
||
difficulty: 'intermediate'
|
||
},
|
||
{
|
||
id: 3,
|
||
word: 'accomplish',
|
||
partOfSpeech: 'verb',
|
||
pronunciation: '/əˈkɒmplɪʃ/',
|
||
translation: '完成、達成',
|
||
definition: 'To finish something successfully or to achieve something',
|
||
example: 'She accomplished her goal of running a marathon.',
|
||
exampleTranslation: '她完成了跑馬拉松的目標。',
|
||
synonyms: ['achieve', 'complete', 'fulfill'],
|
||
difficulty: 'intermediate'
|
||
}
|
||
]
|
||
|
||
const currentCard = cards[currentCardIndex]
|
||
|
||
// Quiz mode options
|
||
const quizOptions = [
|
||
'協商、談判',
|
||
'觀點、看法',
|
||
'完成、達成',
|
||
'建議、提議'
|
||
]
|
||
|
||
const handleFlip = () => {
|
||
setIsFlipped(!isFlipped)
|
||
}
|
||
|
||
const handleNext = () => {
|
||
if (currentCardIndex < cards.length - 1) {
|
||
setCurrentCardIndex(currentCardIndex + 1)
|
||
setIsFlipped(false)
|
||
setSelectedAnswer(null)
|
||
setShowResult(false)
|
||
}
|
||
}
|
||
|
||
const handlePrevious = () => {
|
||
if (currentCardIndex > 0) {
|
||
setCurrentCardIndex(currentCardIndex - 1)
|
||
setIsFlipped(false)
|
||
setSelectedAnswer(null)
|
||
setShowResult(false)
|
||
}
|
||
}
|
||
|
||
const handleDifficultyRate = (rating: number) => {
|
||
// Mock rating logic
|
||
console.log(`Rated ${rating} for ${currentCard.word}`)
|
||
handleNext()
|
||
}
|
||
|
||
const handleQuizAnswer = (answer: string) => {
|
||
setSelectedAnswer(answer)
|
||
setShowResult(true)
|
||
if (answer === currentCard.translation) {
|
||
setScore({ ...score, correct: score.correct + 1, total: score.total + 1 })
|
||
} else {
|
||
setScore({ ...score, total: score.total + 1 })
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||
{/* Navigation */}
|
||
<nav className="bg-white shadow-sm border-b">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="flex justify-between h-16">
|
||
<div className="flex items-center space-x-8">
|
||
<Link href="/dashboard" className="text-2xl font-bold text-primary">DramaLing</Link>
|
||
<div className="hidden md:flex space-x-6">
|
||
<Link href="/dashboard" className="text-gray-600 hover:text-gray-900">儀表板</Link>
|
||
<Link href="/flashcards" className="text-gray-600 hover:text-gray-900">詞卡</Link>
|
||
<Link href="/learn" className="text-gray-900 font-medium">學習</Link>
|
||
<Link href="/generate" className="text-gray-600 hover:text-gray-900">AI 生成</Link>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => window.location.href = '/dashboard'}
|
||
className="text-gray-600 hover:text-gray-900"
|
||
>
|
||
× 結束學習
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||
{/* Progress Bar */}
|
||
<div className="mb-8">
|
||
<div className="flex justify-between items-center mb-2">
|
||
<span className="text-sm text-gray-600">進度</span>
|
||
<span className="text-sm text-gray-600">
|
||
{currentCardIndex + 1} / {cards.length}
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||
<div
|
||
className="bg-primary h-2 rounded-full transition-all"
|
||
style={{ width: `${((currentCardIndex + 1) / cards.length) * 100}%` }}
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mode Toggle */}
|
||
<div className="flex justify-center mb-6">
|
||
<div className="bg-white rounded-lg shadow-sm p-1 inline-flex">
|
||
<button
|
||
onClick={() => setMode('flip')}
|
||
className={`px-4 py-2 rounded-md transition-colors ${
|
||
mode === 'flip'
|
||
? 'bg-primary text-white'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
翻卡模式
|
||
</button>
|
||
<button
|
||
onClick={() => setMode('quiz')}
|
||
className={`px-4 py-2 rounded-md transition-colors ${
|
||
mode === 'quiz'
|
||
? 'bg-primary text-white'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
測驗模式
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{mode === 'flip' ? (
|
||
/* Flip Card Mode */
|
||
<div className="relative">
|
||
<div
|
||
className="relative w-full h-96 cursor-pointer"
|
||
onClick={handleFlip}
|
||
style={{ perspective: '1000px' }}
|
||
>
|
||
<div
|
||
className={`absolute w-full h-full transition-transform duration-600 ${
|
||
isFlipped ? 'rotate-y-180' : ''
|
||
}`}
|
||
style={{
|
||
transformStyle: 'preserve-3d',
|
||
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)'
|
||
}}
|
||
>
|
||
{/* Front of card */}
|
||
<div
|
||
className="absolute w-full h-full bg-white rounded-2xl shadow-xl p-8 flex flex-col items-center justify-center"
|
||
style={{ backfaceVisibility: 'hidden' }}
|
||
>
|
||
<div className="text-4xl font-bold text-gray-900 mb-4">
|
||
{currentCard.word}
|
||
</div>
|
||
<div className="text-lg text-gray-600 mb-2">
|
||
{currentCard.partOfSpeech}
|
||
</div>
|
||
<div className="text-lg text-gray-500">
|
||
{currentCard.pronunciation}
|
||
</div>
|
||
<div className="mt-8 text-sm text-gray-400">
|
||
點擊翻轉查看答案
|
||
</div>
|
||
</div>
|
||
|
||
{/* Back of card */}
|
||
<div
|
||
className="absolute w-full h-full bg-white rounded-2xl shadow-xl p-8 overflow-y-auto"
|
||
style={{
|
||
backfaceVisibility: 'hidden',
|
||
transform: 'rotateY(180deg)'
|
||
}}
|
||
>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<div className="text-sm font-semibold text-gray-700 mb-1">翻譯</div>
|
||
<div className="text-2xl font-bold text-gray-900">{currentCard.translation}</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-semibold text-gray-700 mb-1">定義</div>
|
||
<div className="text-gray-600">{currentCard.definition}</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-semibold text-gray-700 mb-1">例句</div>
|
||
<div className="text-gray-600">{currentCard.example}</div>
|
||
<div className="text-gray-500 text-sm mt-1">{currentCard.exampleTranslation}</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-semibold text-gray-700 mb-1">同義詞</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{currentCard.synonyms.map((syn, idx) => (
|
||
<span key={idx} className="px-3 py-1 bg-gray-100 rounded-full text-sm">
|
||
{syn}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Difficulty Rating */}
|
||
{isFlipped && (
|
||
<div className="mt-8">
|
||
<div className="text-center mb-4">
|
||
<span className="text-gray-600">這個單字對你來說難度如何?</span>
|
||
</div>
|
||
<div className="flex justify-center space-x-3">
|
||
<button
|
||
onClick={() => handleDifficultyRate(1)}
|
||
className="px-4 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors"
|
||
>
|
||
😔 完全不記得
|
||
</button>
|
||
<button
|
||
onClick={() => handleDifficultyRate(3)}
|
||
className="px-4 py-2 bg-yellow-100 text-yellow-700 rounded-lg hover:bg-yellow-200 transition-colors"
|
||
>
|
||
😐 有點困難
|
||
</button>
|
||
<button
|
||
onClick={() => handleDifficultyRate(5)}
|
||
className="px-4 py-2 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors"
|
||
>
|
||
😊 很簡單
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
/* Quiz Mode */
|
||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||
<div className="mb-6">
|
||
<div className="text-sm text-gray-600 mb-2">選擇正確的翻譯</div>
|
||
<div className="text-3xl font-bold text-gray-900">{currentCard.word}</div>
|
||
<div className="text-lg text-gray-500 mt-1">{currentCard.pronunciation}</div>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{quizOptions.map((option, idx) => (
|
||
<button
|
||
key={idx}
|
||
onClick={() => !showResult && handleQuizAnswer(option)}
|
||
disabled={showResult}
|
||
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
|
||
showResult && option === currentCard.translation
|
||
? 'border-green-500 bg-green-50'
|
||
: showResult && option === selectedAnswer && option !== currentCard.translation
|
||
? 'border-red-500 bg-red-50'
|
||
: selectedAnswer === option
|
||
? 'border-primary bg-primary-light'
|
||
: 'border-gray-200 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-medium">{option}</span>
|
||
{showResult && option === currentCard.translation && (
|
||
<svg className="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
)}
|
||
{showResult && option === selectedAnswer && option !== currentCard.translation && (
|
||
<svg className="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
)}
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{showResult && (
|
||
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
||
<div className="text-sm font-semibold text-gray-700 mb-2">例句</div>
|
||
<div className="text-gray-600">{currentCard.example}</div>
|
||
<div className="text-gray-500 text-sm mt-1">{currentCard.exampleTranslation}</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-6 text-center">
|
||
<div className="text-sm text-gray-600">
|
||
正確率:{score.total > 0 ? Math.round((score.correct / score.total) * 100) : 0}%
|
||
({score.correct}/{score.total})
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Navigation Buttons */}
|
||
<div className="flex justify-between mt-8">
|
||
<button
|
||
onClick={handlePrevious}
|
||
disabled={currentCardIndex === 0}
|
||
className="flex items-center space-x-2 px-6 py-3 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||
</svg>
|
||
<span>上一個</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleNext}
|
||
disabled={currentCardIndex === cards.length - 1}
|
||
className="flex items-center space-x-2 px-6 py-3 bg-primary text-white rounded-lg shadow-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<span>下一個</span>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
} |