840 lines
35 KiB
TypeScript
840 lines
35 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import Link from 'next/link'
|
||
import { useRouter } from 'next/navigation'
|
||
import { Navigation } from '@/components/Navigation'
|
||
|
||
export default function LearnPage() {
|
||
const router = useRouter()
|
||
const [currentCardIndex, setCurrentCardIndex] = useState(0)
|
||
const [isFlipped, setIsFlipped] = useState(false)
|
||
const [mode, setMode] = useState<'flip' | 'quiz' | 'fill' | 'listening' | 'speaking'>('flip')
|
||
const [score, setScore] = useState({ correct: 0, total: 0 })
|
||
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
|
||
const [showResult, setShowResult] = useState(false)
|
||
const [fillAnswer, setFillAnswer] = useState('')
|
||
const [showHint, setShowHint] = useState(false)
|
||
const [isRecording, setIsRecording] = useState(false)
|
||
const [audioPlaying, setAudioPlaying] = useState(false)
|
||
const [modalImage, setModalImage] = useState<string | null>(null)
|
||
const [showReportModal, setShowReportModal] = useState(false)
|
||
const [reportReason, setReportReason] = useState('')
|
||
const [reportingCard, setReportingCard] = useState<any>(null)
|
||
|
||
// Mock data with real example images
|
||
const cards = [
|
||
{
|
||
id: 1,
|
||
word: 'brought',
|
||
partOfSpeech: 'verb',
|
||
pronunciation: '/brɔːt/',
|
||
translation: '提出、帶來',
|
||
definition: 'Past tense of bring; to mention or introduce a topic in conversation',
|
||
example: 'He brought this thing up during our meeting and no one agreed.',
|
||
exampleTranslation: '他在我們的會議中提出了這件事,但沒有人同意。',
|
||
exampleImage: '/images/examples/bring_up.png',
|
||
synonyms: ['mentioned', 'raised', 'introduced'],
|
||
difficulty: 'B1'
|
||
},
|
||
{
|
||
id: 2,
|
||
word: 'instincts',
|
||
partOfSpeech: 'noun',
|
||
pronunciation: '/ˈɪnstɪŋkts/',
|
||
translation: '本能、直覺',
|
||
definition: 'Natural abilities that help living things survive without learning',
|
||
example: 'Animals use their instincts to find food and stay safe.',
|
||
exampleTranslation: '動物利用本能來尋找食物並保持安全。',
|
||
exampleImage: '/images/examples/instinct.png',
|
||
synonyms: ['intuition', 'impulse', 'tendency'],
|
||
difficulty: 'B2'
|
||
},
|
||
{
|
||
id: 3,
|
||
word: 'warrants',
|
||
partOfSpeech: 'noun',
|
||
pronunciation: '/ˈwɔːrənts/',
|
||
translation: '搜查令、授權令',
|
||
definition: 'Official documents that give police permission to do something',
|
||
example: 'The police obtained warrants to search the building.',
|
||
exampleTranslation: '警方取得了搜查令來搜查這棟建築物。',
|
||
exampleImage: '/images/examples/warrant.png',
|
||
synonyms: ['authorization', 'permit', 'license'],
|
||
difficulty: 'C1'
|
||
}
|
||
]
|
||
|
||
const currentCard = cards[currentCardIndex]
|
||
|
||
// Quiz mode options - dynamically generate from current cards
|
||
const quizOptions = [
|
||
cards[currentCardIndex].translation,
|
||
...cards
|
||
.filter((_, idx) => idx !== currentCardIndex)
|
||
.map(card => card.translation)
|
||
.slice(0, 2),
|
||
'建議、提議' // additional wrong option
|
||
].sort(() => Math.random() - 0.5) // shuffle options
|
||
|
||
const handleFlip = () => {
|
||
setIsFlipped(!isFlipped)
|
||
}
|
||
|
||
const handleNext = () => {
|
||
if (currentCardIndex < cards.length - 1) {
|
||
setCurrentCardIndex(currentCardIndex + 1)
|
||
setIsFlipped(false)
|
||
setSelectedAnswer(null)
|
||
setShowResult(false)
|
||
setFillAnswer('')
|
||
setShowHint(false)
|
||
}
|
||
}
|
||
|
||
const handlePrevious = () => {
|
||
if (currentCardIndex > 0) {
|
||
setCurrentCardIndex(currentCardIndex - 1)
|
||
setIsFlipped(false)
|
||
setSelectedAnswer(null)
|
||
setShowResult(false)
|
||
setFillAnswer('')
|
||
setShowHint(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 */}
|
||
<Navigation
|
||
showExitLearning={true}
|
||
onExitLearning={() => router.push('/dashboard')}
|
||
/>
|
||
|
||
<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 flex-wrap">
|
||
<button
|
||
onClick={() => setMode('flip')}
|
||
className={`px-3 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-3 py-2 rounded-md transition-colors ${
|
||
mode === 'quiz'
|
||
? 'bg-primary text-white'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
選擇題
|
||
</button>
|
||
<button
|
||
onClick={() => setMode('fill')}
|
||
className={`px-3 py-2 rounded-md transition-colors ${
|
||
mode === 'fill'
|
||
? 'bg-primary text-white'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
填空題
|
||
</button>
|
||
<button
|
||
onClick={() => setMode('listening')}
|
||
className={`px-3 py-2 rounded-md transition-colors ${
|
||
mode === 'listening'
|
||
? 'bg-primary text-white'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
聽力測試
|
||
</button>
|
||
<button
|
||
onClick={() => setMode('speaking')}
|
||
className={`px-3 py-2 rounded-md transition-colors ${
|
||
mode === 'speaking'
|
||
? 'bg-primary text-white'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
口說測試
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{mode === 'flip' ? (
|
||
/* Flip Card Mode */
|
||
<div className="relative">
|
||
{/* Error Report Button for Flip Mode */}
|
||
<div className="flex justify-end mb-2">
|
||
<button
|
||
onClick={() => {
|
||
setReportingCard(currentCard)
|
||
setShowReportModal(true)
|
||
}}
|
||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||
title="回報錯誤"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
回報錯誤
|
||
</button>
|
||
</div>
|
||
|
||
<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>
|
||
) : mode === 'quiz' ? (
|
||
/* Quiz Mode - 選擇題:英文定義選中文翻譯 */
|
||
<div className="relative">
|
||
{/* Error Report Button for Quiz Mode */}
|
||
<div className="flex justify-end mb-2">
|
||
<button
|
||
onClick={() => {
|
||
setReportingCard(currentCard)
|
||
setShowReportModal(true)
|
||
}}
|
||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||
title="回報錯誤"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
回報錯誤
|
||
</button>
|
||
</div>
|
||
|
||
<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-xl text-gray-800 leading-relaxed">
|
||
{currentCard.definition}
|
||
</div>
|
||
<div className="text-sm text-gray-500 mt-2">
|
||
({currentCard.partOfSpeech})
|
||
</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>
|
||
|
||
</div>
|
||
</div>
|
||
) : mode === 'fill' ? (
|
||
/* Fill in the Blank Mode - 填空題 */
|
||
<div className="relative">
|
||
{/* Error Report Button for Fill Mode */}
|
||
<div className="flex justify-end mb-2">
|
||
<button
|
||
onClick={() => {
|
||
setReportingCard(currentCard)
|
||
setShowReportModal(true)
|
||
}}
|
||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||
title="回報錯誤"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
回報錯誤
|
||
</button>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||
<div className="mb-6">
|
||
<div className="text-sm text-gray-600 mb-4">根據例句圖片和句子填入正確的詞彙</div>
|
||
|
||
{/* Example Image */}
|
||
{currentCard.exampleImage && (
|
||
<div className="mb-4">
|
||
<img
|
||
src={currentCard.exampleImage}
|
||
alt="Example context"
|
||
className="w-full rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-shadow"
|
||
style={{ maxHeight: '400px', objectFit: 'contain' }}
|
||
onClick={() => setModalImage(currentCard.exampleImage)}
|
||
/>
|
||
<div className="text-xs text-gray-500 mt-2 text-center">點擊圖片可放大查看</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Example Sentence with Blank */}
|
||
<div className="text-lg text-gray-800 mb-4">
|
||
{currentCard.example.split(currentCard.word).map((part, i) => (
|
||
<span key={i}>
|
||
{part}
|
||
{i < currentCard.example.split(currentCard.word).length - 1 && (
|
||
<span className="inline-block w-32 border-b-2 border-gray-400 mx-1"></span>
|
||
)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
|
||
{/* Hint Button */}
|
||
{!showHint && (
|
||
<button
|
||
onClick={() => setShowHint(true)}
|
||
className="text-sm text-primary hover:text-primary-hover flex items-center gap-1"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
點擊查看提示
|
||
</button>
|
||
)}
|
||
|
||
{/* Definition Hint */}
|
||
{showHint && (
|
||
<div className="mt-3 p-3 bg-blue-50 rounded-lg">
|
||
<div className="text-sm text-blue-800">
|
||
<strong>定義提示:</strong> {currentCard.definition}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Answer Input */}
|
||
<div className="mb-6">
|
||
<input
|
||
type="text"
|
||
value={fillAnswer}
|
||
onChange={(e) => setFillAnswer(e.target.value)}
|
||
placeholder="輸入答案..."
|
||
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-primary focus:outline-none text-lg"
|
||
onKeyPress={(e) => {
|
||
if (e.key === 'Enter' && fillAnswer) {
|
||
setShowResult(true)
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* Submit Button */}
|
||
{!showResult && (
|
||
<button
|
||
onClick={() => fillAnswer && setShowResult(true)}
|
||
disabled={!fillAnswer}
|
||
className="w-full py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
提交答案
|
||
</button>
|
||
)}
|
||
|
||
{/* Result Display */}
|
||
{showResult && (
|
||
<div className={`p-4 rounded-lg ${
|
||
fillAnswer.toLowerCase() === currentCard.word.toLowerCase()
|
||
? 'bg-green-50 border-2 border-green-500'
|
||
: 'bg-red-50 border-2 border-red-500'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className={`font-semibold ${
|
||
fillAnswer.toLowerCase() === currentCard.word.toLowerCase()
|
||
? 'text-green-700'
|
||
: 'text-red-700'
|
||
}`}>
|
||
{fillAnswer.toLowerCase() === currentCard.word.toLowerCase() ? '✓ 正確!' : '✗ 錯誤'}
|
||
</span>
|
||
</div>
|
||
{fillAnswer.toLowerCase() !== currentCard.word.toLowerCase() && (
|
||
<div className="text-sm text-gray-700">
|
||
正確答案:<span className="font-bold">{currentCard.word}</span>
|
||
</div>
|
||
)}
|
||
<div className="mt-3 text-sm text-gray-600">
|
||
<div className="font-semibold mb-1">完整例句:</div>
|
||
<div>{currentCard.example}</div>
|
||
<div className="text-gray-500 mt-1">{currentCard.exampleTranslation}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : mode === 'listening' ? (
|
||
/* Listening Test Mode - 聽力測試 */
|
||
<div className="relative">
|
||
{/* Error Report Button for Listening Mode */}
|
||
<div className="flex justify-end mb-2">
|
||
<button
|
||
onClick={() => {
|
||
setReportingCard(currentCard)
|
||
setShowReportModal(true)
|
||
}}
|
||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||
title="回報錯誤"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
回報錯誤
|
||
</button>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||
<div className="mb-6 text-center">
|
||
<div className="text-sm text-gray-600 mb-4">聽音頻,選擇正確的單字</div>
|
||
|
||
{/* Audio Play Button */}
|
||
<button
|
||
onClick={() => {
|
||
setAudioPlaying(true)
|
||
// Simulate audio playing
|
||
setTimeout(() => setAudioPlaying(false), 2000)
|
||
}}
|
||
className="mx-auto mb-6 p-8 bg-gray-100 rounded-full hover:bg-gray-200 transition-colors"
|
||
>
|
||
{audioPlaying ? (
|
||
<svg className="w-16 h-16 text-primary animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
|
||
</svg>
|
||
) : (
|
||
<svg className="w-16 h-16 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
)}
|
||
</button>
|
||
|
||
<div className="text-sm text-gray-500">點擊播放按鈕聽發音</div>
|
||
</div>
|
||
|
||
{/* Word Options */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => (
|
||
<button
|
||
key={word}
|
||
onClick={() => !showResult && handleQuizAnswer(word)}
|
||
disabled={showResult}
|
||
className={`p-4 text-lg font-medium rounded-lg border-2 transition-all ${
|
||
showResult && word === currentCard.word
|
||
? 'border-green-500 bg-green-50'
|
||
: showResult && word === selectedAnswer && word !== currentCard.word
|
||
? 'border-red-500 bg-red-50'
|
||
: selectedAnswer === word
|
||
? 'border-primary bg-primary-light'
|
||
: 'border-gray-200 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
{word}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Result Display */}
|
||
{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="space-y-2">
|
||
<div>
|
||
<span className="font-semibold">{currentCard.word}</span> - {currentCard.translation}
|
||
</div>
|
||
<div className="text-sm text-gray-600">{currentCard.definition}</div>
|
||
<div className="text-sm text-gray-500 italic">"{currentCard.example}"</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : mode === 'speaking' ? (
|
||
/* Speaking Test Mode - 口說測試 */
|
||
<div className="relative">
|
||
{/* Error Report Button for Speaking Mode */}
|
||
<div className="flex justify-end mb-2">
|
||
<button
|
||
onClick={() => {
|
||
setReportingCard(currentCard)
|
||
setShowReportModal(true)
|
||
}}
|
||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||
title="回報錯誤"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
回報錯誤
|
||
</button>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||
<div className="mb-6">
|
||
<div className="text-sm text-gray-600 mb-4">念出以下例句</div>
|
||
|
||
{/* Target Sentence */}
|
||
<div className="p-6 bg-gray-50 rounded-lg mb-6">
|
||
<div className="text-xl text-gray-800 leading-relaxed mb-3">
|
||
{currentCard.example}
|
||
</div>
|
||
<div className="text-gray-600">
|
||
{currentCard.exampleTranslation}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pronunciation Guide */}
|
||
<div className="mb-6">
|
||
<div className="text-sm text-gray-600 mb-2">重點單字發音:</div>
|
||
<div className="flex items-center gap-4">
|
||
<span className="font-semibold text-lg">{currentCard.word}</span>
|
||
<span className="text-gray-500">{currentCard.pronunciation}</span>
|
||
<button className="text-primary hover:text-primary-hover">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recording Button */}
|
||
<div className="text-center">
|
||
<button
|
||
onClick={() => {
|
||
setIsRecording(!isRecording)
|
||
if (!isRecording) {
|
||
// Start recording
|
||
setTimeout(() => {
|
||
setIsRecording(false)
|
||
setShowResult(true)
|
||
}, 3000)
|
||
}
|
||
}}
|
||
className={`p-6 rounded-full transition-all ${
|
||
isRecording
|
||
? 'bg-red-500 hover:bg-red-600 animate-pulse'
|
||
: 'bg-primary hover:bg-primary-hover'
|
||
}`}
|
||
>
|
||
{isRecording ? (
|
||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||
</svg>
|
||
) : (
|
||
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
|
||
</svg>
|
||
)}
|
||
</button>
|
||
<div className="mt-3 text-sm text-gray-600">
|
||
{isRecording ? '錄音中... 點擊停止' : '點擊開始錄音'}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Result Display */}
|
||
{showResult && (
|
||
<div className="mt-6 p-4 bg-green-50 border-2 border-green-500 rounded-lg">
|
||
<div className="text-green-700 font-semibold mb-2">
|
||
✓ 完成口說練習!
|
||
</div>
|
||
<div className="text-sm text-gray-600">
|
||
提示:持續練習可以提高發音準確度和流暢度
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{/* 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>
|
||
|
||
{/* Image Modal */}
|
||
{modalImage && (
|
||
<div
|
||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4"
|
||
onClick={() => setModalImage(null)}
|
||
>
|
||
<div
|
||
className="relative max-w-4xl max-h-[90vh] bg-white rounded-lg overflow-hidden"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
{/* Close Button */}
|
||
<button
|
||
onClick={() => setModalImage(null)}
|
||
className="absolute top-2 right-2 z-10 p-2 bg-white bg-opacity-90 rounded-full hover:bg-opacity-100 transition-all shadow-lg"
|
||
>
|
||
<svg className="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
|
||
{/* Image */}
|
||
<div className="p-4">
|
||
<img
|
||
src={modalImage}
|
||
alt="Example context enlarged"
|
||
className="w-full h-full object-contain"
|
||
style={{ maxHeight: 'calc(90vh - 2rem)' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error Report Modal */}
|
||
{showReportModal && (
|
||
<div
|
||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4"
|
||
onClick={() => setShowReportModal(false)}
|
||
>
|
||
<div
|
||
className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">回報錯誤</h3>
|
||
<button
|
||
onClick={() => setShowReportModal(false)}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<div className="text-sm text-gray-600 mb-2">
|
||
詞卡:<span className="font-medium">{reportingCard?.word}</span>
|
||
</div>
|
||
<div className="text-sm text-gray-600 mb-2">
|
||
測驗模式:{mode === 'flip' ? '翻卡模式' : mode === 'quiz' ? '選擇題' : mode === 'fill' ? '填空題' : mode === 'listening' ? '聽力測試' : '口說測試'}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
錯誤原因(選填)
|
||
</label>
|
||
<textarea
|
||
value={reportReason}
|
||
onChange={(e) => setReportReason(e.target.value)}
|
||
placeholder="請描述錯誤內容..."
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => {
|
||
// Submit error report
|
||
console.log('Error reported:', {
|
||
card: reportingCard,
|
||
mode,
|
||
reason: reportReason
|
||
})
|
||
setShowReportModal(false)
|
||
setReportReason('')
|
||
setReportingCard(null)
|
||
// Show success message (could add a toast notification here)
|
||
alert('感謝您的回報,我們會盡快處理!')
|
||
}}
|
||
className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors"
|
||
>
|
||
送出回報
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowReportModal(false)
|
||
setReportReason('')
|
||
setReportingCard(null)
|
||
}}
|
||
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
} |