dramaling-vocab-learning/frontend/app/learn/page.tsx

895 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { Navigation } from '@/components/Navigation'
import AudioPlayer from '@/components/AudioPlayer'
import VoiceRecorder from '@/components/VoiceRecorder'
import LearningComplete from '@/components/LearningComplete'
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)
const [showComplete, setShowComplete] = useState(false)
// 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: '/ˈː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].word,
...cards
.filter((_, idx) => idx !== currentCardIndex)
.map(card => card.word)
.slice(0, 2),
'negotiate' // 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)
} else {
// Learning session complete
setShowComplete(true)
}
}
const handlePrevious = () => {
if (currentCardIndex > 0) {
setCurrentCardIndex(currentCardIndex - 1)
setIsFlipped(false)
setSelectedAnswer(null)
setShowResult(false)
setFillAnswer('')
setShowHint(false)
}
}
const handleDifficultyRate = (rating: number) => {
// Update score based on difficulty rating
console.log(`Rated ${rating} for ${currentCard.word}`)
// SM-2 Algorithm simulation
if (rating >= 4) {
setScore({ ...score, correct: score.correct + 1, total: score.total + 1 })
} else {
setScore({ ...score, total: score.total + 1 })
}
// Auto advance after rating
setTimeout(() => {
handleNext()
}, 500)
}
const handleQuizAnswer = (answer: string) => {
setSelectedAnswer(answer)
setShowResult(true)
if (answer === currentCard.word) {
setScore({ ...score, correct: score.correct + 1, total: score.total + 1 })
} else {
setScore({ ...score, total: score.total + 1 })
}
}
const handleFillAnswer = () => {
if (fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()) {
setScore({ ...score, correct: score.correct + 1, total: score.total + 1 })
} else {
setScore({ ...score, total: score.total + 1 })
}
setShowResult(true)
}
const handleListeningAnswer = (word: string) => {
setSelectedAnswer(word)
setShowResult(true)
if (word === currentCard.word) {
setScore({ ...score, correct: score.correct + 1, total: score.total + 1 })
} else {
setScore({ ...score, total: score.total + 1 })
}
}
const handleRestart = () => {
setCurrentCardIndex(0)
setIsFlipped(false)
setSelectedAnswer(null)
setShowResult(false)
setFillAnswer('')
setShowHint(false)
setScore({ correct: 0, total: 0 })
setShowComplete(false)
}
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>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">
{currentCardIndex + 1} / {cards.length}
</span>
<div className="text-sm">
<span className="text-green-600 font-semibold">{score.correct}</span>
<span className="text-gray-500">/</span>
<span className="text-gray-600">{score.total}</span>
{score.total > 0 && (
<span className="text-blue-600 ml-2">
({Math.round((score.correct / score.total) * 100)}%)
</span>
)}
</div>
</div>
</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-gray-500 hover:text-gray-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 min-h-96 cursor-pointer"
onClick={handleFlip}
style={{ perspective: '1000px' }}
>
<div
className={`w-full transition-transform duration-600 ${
isFlipped ? 'rotate-y-180' : ''
}`}
style={{
transformStyle: 'preserve-3d',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)'
}}
>
{/* Front of card */}
<div
className="w-full min-h-96 bg-white rounded-2xl shadow-xl p-8 flex flex-col items-center justify-center"
style={{
backfaceVisibility: 'hidden',
display: isFlipped ? 'none' : 'flex'
}}
>
<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="flex items-center justify-center gap-4 mb-4">
<div className="text-lg text-gray-500">
{currentCard.pronunciation}
</div>
<AudioPlayer
text={currentCard.word}
className="flex-shrink-0"
/>
</div>
<div className="mt-8 text-sm text-gray-400">
</div>
</div>
{/* Back of card */}
<div
className="w-full bg-white rounded-2xl shadow-xl p-6"
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
display: isFlipped ? 'block' : 'none'
}}
>
<div className="space-y-3">
<div className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-semibold text-gray-700 mb-1"></div>
<div className="text-xl font-bold text-gray-900">{currentCard.translation}</div>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-semibold text-gray-700 mb-1"></div>
<div className="text-gray-600 text-sm">{currentCard.definition}</div>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-semibold text-gray-700 mb-1"></div>
<div className="text-gray-600 text-sm mb-2">{currentCard.example}</div>
<div className="text-gray-500 text-xs mb-2">{currentCard.exampleTranslation}</div>
<AudioPlayer
text={currentCard.example}
className="mt-1"
/>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<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-2 py-1 bg-gray-100 rounded-full text-xs">
{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-gray-500 hover:text-gray-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>
<div className="p-4 bg-gray-50 rounded-lg">
<div className="text-xl text-gray-800 leading-relaxed mb-3">
{currentCard.definition}
</div>
<div className="text-sm text-gray-500 mb-3">
({currentCard.partOfSpeech})
</div>
<AudioPlayer
text={currentCard.definition}
className="mt-2"
/>
</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.word
? 'border-green-500 bg-green-50'
: showResult && option === selectedAnswer && option !== currentCard.word
? '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.word && (
<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.word && (
<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-gray-500 hover:text-gray-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) {
handleFillAnswer()
}
}}
/>
</div>
{/* Submit Button */}
{!showResult && (
<button
onClick={() => fillAnswer && handleFillAnswer()}
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 className="mb-2">{currentCard.example}</div>
<div className="text-gray-500 mb-3">{currentCard.exampleTranslation}</div>
<AudioPlayer
text={currentCard.example}
className="mt-2"
/>
</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-gray-500 hover:text-gray-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 Player */}
<div className="flex flex-col items-center mb-6">
<AudioPlayer
text={currentCard.word}
className="mb-4"
/>
<div className="text-sm text-gray-500">
</div>
</div>
</div>
{/* Word Options */}
<div className="grid grid-cols-2 gap-3">
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => (
<button
key={word}
onClick={() => !showResult && handleListeningAnswer(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="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-gray-500 hover:text-gray-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>
<AudioPlayer
text={currentCard.word}
className="flex-shrink-0"
/>
</div>
<div className="mt-3">
<div className="text-sm text-gray-600 mb-2"></div>
<AudioPlayer
text={currentCard.example}
className="flex-shrink-0"
/>
</div>
</div>
{/* Voice Recorder */}
<VoiceRecorder
targetText={currentCard.example}
onScoreReceived={(score) => {
console.log('Pronunciation score:', score);
setShowResult(true);
}}
onRecordingComplete={(audioBlob) => {
console.log('Recording completed:', audioBlob);
}}
maxDuration={30}
userLevel="B1"
className="mt-4"
/>
</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>
)}
{/* Learning Complete Modal */}
{showComplete && (
<LearningComplete
score={score}
mode={mode}
onRestart={handleRestart}
onBackToDashboard={() => router.push('/dashboard')}
/>
)}
</div>
)
}