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

840 lines
35 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'
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: '/ˈː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>
)
}