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

1370 lines
53 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, useEffect, useRef, useLayoutEffect } from 'react'
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 [mounted, setMounted] = useState(false)
const [currentCardIndex, setCurrentCardIndex] = useState(0)
const [isFlipped, setIsFlipped] = useState(false)
const [mode, setMode] = useState<'flip-memory' | 'vocab-choice' | 'vocab-listening' | 'sentence-listening' | 'sentence-fill' | 'sentence-reorder' | 'sentence-speaking'>('flip-memory')
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 [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)
const [quizOptions, setQuizOptions] = useState<string[]>(['brought', 'determine', 'achieve', 'consider'])
const [cardHeight, setCardHeight] = useState<number>(400)
// Sentence reorder states
const [shuffledWords, setShuffledWords] = useState<string[]>([])
const [arrangedWords, setArrangedWords] = useState<string[]>([])
const [reorderResult, setReorderResult] = useState<boolean | null>(null)
// Refs for measuring card content heights
const cardFrontRef = useRef<HTMLDivElement>(null)
const cardBackRef = useRef<HTMLDivElement>(null)
const cardContainerRef = useRef<HTMLDivElement>(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]
// Calculate optimal card height based on content (only when card changes)
const calculateCardHeight = () => {
if (!cardFrontRef.current || !cardBackRef.current) return 400;
// Get the scroll heights to measure actual content
const frontHeight = cardFrontRef.current.scrollHeight;
const backHeight = cardBackRef.current.scrollHeight;
console.log('Heights calculated:', { frontHeight, backHeight }); // Debug log
// Use the maximum height with padding
const maxHeight = Math.max(frontHeight, backHeight);
const paddedHeight = maxHeight + 40; // Add padding for visual spacing
// Ensure minimum height for visual consistency
return Math.max(paddedHeight, 450);
};
// Update card height only when card content changes (not on flip)
useLayoutEffect(() => {
if (mounted && cardFrontRef.current && cardBackRef.current) {
// Wait for DOM to be fully rendered
const timer = setTimeout(() => {
const newHeight = calculateCardHeight();
setCardHeight(newHeight);
}, 50);
return () => clearTimeout(timer);
}
}, [currentCardIndex, mounted]);
// Client-side mounting
useEffect(() => {
setMounted(true)
}, [])
// Quiz options generation
useEffect(() => {
const currentWord = cards[currentCardIndex].word;
// Generate quiz options with current word and other words
const otherWords = cards
.filter((_, idx) => idx !== currentCardIndex)
.map(card => card.word);
// If we don't have enough words in the deck, add some default options
const additionalOptions = ['determine', 'achieve', 'consider', 'negotiate', 'establish', 'maintain'];
const allOtherWords = [...otherWords, ...additionalOptions];
// Take 3 other words (avoiding duplicates)
const selectedOtherWords: string[] = [];
for (const word of allOtherWords) {
if (selectedOtherWords.length >= 3) break;
if (word !== currentWord && !selectedOtherWords.includes(word)) {
selectedOtherWords.push(word);
}
}
// Ensure we have exactly 4 options: current word + 3 others
const options = [currentWord, ...selectedOtherWords].sort(() => Math.random() - 0.5);
setQuizOptions(options);
// Reset quiz state when card changes
setSelectedAnswer(null);
setShowResult(false);
}, [currentCardIndex])
// Initialize sentence reorder when card changes or mode switches to sentence-reorder
useEffect(() => {
if (mode === 'sentence-reorder') {
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
setReorderResult(null)
}
}, [currentCardIndex, mode, currentCard.example])
// Sentence reorder handlers
const handleWordClick = (word: string) => {
// Move word from shuffled to arranged
setShuffledWords(prev => prev.filter(w => w !== word))
setArrangedWords(prev => [...prev, word])
setReorderResult(null)
}
const handleRemoveFromArranged = (word: string) => {
setArrangedWords(prev => prev.filter(w => w !== word))
setShuffledWords(prev => [...prev, word])
setReorderResult(null)
}
const handleCheckReorderAnswer = () => {
const userSentence = arrangedWords.join(' ')
const correctSentence = currentCard.example
const isCorrect = userSentence.toLowerCase().trim() === correctSentence.toLowerCase().trim()
setReorderResult(isCorrect)
}
const handleResetReorder = () => {
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
setReorderResult(null)
}
const handleFlip = () => {
setIsFlipped(!isFlipped)
}
const handleNext = () => {
if (currentCardIndex < cards.length - 1) {
setCurrentCardIndex(currentCardIndex + 1)
setIsFlipped(false)
setSelectedAnswer(null)
setShowResult(false)
setFillAnswer('')
setShowHint(false)
// Height will be recalculated in useLayoutEffect
} else {
setShowComplete(true)
}
}
const handlePrevious = () => {
if (currentCardIndex > 0) {
setCurrentCardIndex(currentCardIndex - 1)
setIsFlipped(false)
setSelectedAnswer(null)
setShowResult(false)
setFillAnswer('')
setShowHint(false)
// Height will be recalculated in useLayoutEffect
}
}
const handleQuizAnswer = (answer: string) => {
if (showResult) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.word
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
}
const handleFillAnswer = () => {
if (showResult) return
setShowResult(true)
const isCorrect = fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
}
const handleListeningAnswer = (answer: string) => {
if (showResult) return
setSelectedAnswer(answer)
setShowResult(true)
const isCorrect = answer === currentCard.word
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
}
const handleSpeakingAnswer = (transcript: string) => {
setShowResult(true)
const isCorrect = transcript.toLowerCase().includes(currentCard.word.toLowerCase())
setScore(prev => ({
correct: isCorrect ? prev.correct + 1 : prev.correct,
total: prev.total + 1
}))
}
const handleReportSubmit = () => {
console.log('Report submitted:', {
card: reportingCard,
reason: reportReason
})
setShowReportModal(false)
setReportReason('')
setReportingCard(null)
}
const handleRestart = () => {
setCurrentCardIndex(0)
setIsFlipped(false)
setSelectedAnswer(null)
setShowResult(false)
setFillAnswer('')
setShowHint(false)
setScore({ correct: 0, total: 0 })
setShowComplete(false)
}
// Show loading screen until mounted
if (!mounted) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-gray-500 text-lg">...</div>
</div>
)
}
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-memory')}
className={`px-3 py-2 rounded-md transition-colors ${
mode === 'flip-memory'
? 'bg-primary text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
</button>
<button
onClick={() => setMode('vocab-choice')}
className={`px-3 py-2 rounded-md transition-colors ${
mode === 'vocab-choice'
? 'bg-primary text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
</button>
<button
onClick={() => setMode('vocab-listening')}
className={`px-3 py-2 rounded-md transition-colors ${
mode === 'vocab-listening'
? 'bg-primary text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
</button>
<button
onClick={() => setMode('sentence-listening')}
className={`px-3 py-2 rounded-md transition-colors ${
mode === 'sentence-listening'
? 'bg-primary text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
</button>
<button
onClick={() => setMode('sentence-fill')}
className={`px-3 py-2 rounded-md transition-colors ${
mode === 'sentence-fill'
? 'bg-primary text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
</button>
<button
onClick={() => setMode('sentence-reorder')}
className={`px-3 py-2 rounded-md transition-colors ${
mode === 'sentence-reorder'
? 'bg-primary text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
</button>
<button
onClick={() => setMode('sentence-speaking')}
className={`px-3 py-2 rounded-md transition-colors ${
mode === 'sentence-speaking'
? 'bg-primary text-white'
: 'text-gray-600 hover:text-gray-900'
}`}
>
</button>
</div>
</div>
{mode === 'flip-memory' ? (
/* 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="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div
ref={cardContainerRef}
className="card-container"
onClick={handleFlip}
style={{ height: `${cardHeight}px` }}
>
<div className={`card ${isFlipped ? 'flipped' : ''}`}>
{/* Front */}
<div className="card-front">
<div
ref={cardFrontRef}
className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow p-8"
>
{/* Title and Instructions */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficulty}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Word Display */}
<div className="flex-1 flex items-center justify-center mt-6">
<div className="bg-gray-50 rounded-lg p-8 w-full text-center">
<h3 className="text-4xl font-bold text-gray-900 mb-6">
{currentCard.word}
</h3>
<div className="flex items-center justify-center gap-3">
<span className="text-lg text-gray-500">
{currentCard.pronunciation}
</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
</div>
</div>
{/* Back */}
<div className="card-back">
<div
ref={cardBackRef}
className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow"
>
{/* Content Sections */}
<div className="space-y-4">
{/* Definition */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 text-left">{currentCard.definition}</p>
</div>
{/* Example */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="relative">
<p className="text-gray-700 italic mb-2 text-left pr-12">"{currentCard.example}"</p>
<div className="absolute bottom-0 right-0">
<AudioPlayer text={currentCard.example} />
</div>
</div>
<p className="text-gray-600 text-sm text-left">"{currentCard.exampleTranslation}"</p>
</div>
{/* Synonyms */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="flex flex-wrap gap-2">
{currentCard.synonyms.map((synonym, index) => (
<span
key={index}
className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200"
>
{synonym}
</span>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === cards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'vocab-choice' ? (
/* Vocab Choice Mode - 詞彙選擇 */
<div className="relative">
{/* Error Report Button for Quiz Mode */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficulty}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
<div className="text-center mb-8">
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 text-left">{currentCard.definition}</p>
</div>
</div>
<div className="space-y-3 mb-6">
{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 text-green-700'
: option === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
{option}
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.word
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.word
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.word && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<div className="flex items-center text-gray-600">
<strong></strong>
<span className="mx-2">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === cards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-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="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficulty}
</span>
</div>
{/* Example Image */}
{currentCard.exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={currentCard.exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(currentCard.exampleImage)}
/>
</div>
</div>
)}
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Example Sentence with Blanks */}
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-6">
<div className="text-lg text-gray-700 leading-relaxed">
{currentCard.example.split(new RegExp(`(${currentCard.word})`, 'gi')).map((part, index) => {
const isTargetWord = part.toLowerCase() === currentCard.word.toLowerCase();
return isTargetWord ? (
<span key={index} className="relative inline-block mx-1">
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !showResult && fillAnswer.trim()) {
handleFillAnswer()
}
}}
placeholder=""
disabled={showResult}
className={`inline-block px-2 py-1 text-center bg-transparent focus:outline-none disabled:bg-gray-100 ${
fillAnswer
? 'border-b-2 border-blue-500'
: 'border-b-2 border-dashed border-gray-400 focus:border-blue-400 focus:border-solid'
}`}
style={{ width: `${Math.max(100, Math.max(currentCard.word.length * 12, fillAnswer.length * 12 + 20))}px` }}
/>
{!fillAnswer && (
<span
className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
style={{ paddingBottom: '8px' }}
>
____
</span>
)}
</span>
) : (
<span key={index}>{part}</span>
);
})}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3 mb-4">
{!showResult && fillAnswer.trim() && (
<button
onClick={handleFillAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
</button>
)}
<button
onClick={() => setShowHint(!showHint)}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
{showHint ? '隱藏提示' : '顯示提示'}
</button>
</div>
{/* Hint Section */}
{showHint && (
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-semibold text-yellow-800 mb-2"></h4>
<p className="text-yellow-800">{currentCard.definition}</p>
</div>
)}
{showResult && (
<div className={`mt-6 p-6 rounded-lg w-full ${
fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase()
? 'text-green-700'
: 'text-red-700'
}`}>
{fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase() ? '正確!' : '錯誤!'}
</p>
{fillAnswer.toLowerCase().trim() !== currentCard.word.toLowerCase() && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
<span className="mx-2">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</p>
</div>
<div className="text-left">
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === cards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'vocab-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="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
()
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficulty}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Content Sections */}
<div className="space-y-4 mb-8">
{/* Audio */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="flex items-center gap-3">
<span className="text-gray-700">{currentCard.pronunciation}</span>
<AudioPlayer text={currentCard.word} />
</div>
</div>
</div>
{/* Word Options */}
<div className="grid grid-cols-2 gap-3 mb-6">
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => (
<button
key={word}
onClick={() => !showResult && handleListeningAnswer(word)}
disabled={showResult}
className={`p-4 text-center rounded-lg border-2 transition-all ${
showResult
? word === currentCard.word
? 'border-green-500 bg-green-50 text-green-700'
: word === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
{word}
</button>
))}
</div>
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === currentCard.word
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === currentCard.word
? 'text-green-700'
: 'text-red-700'
}`}>
{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== currentCard.word && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{currentCard.word}</strong>
</p>
<p className="text-gray-600 text-left mt-1">
{currentCard.pronunciation}
</p>
</div>
)}
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === cards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-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="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficulty}
</span>
</div>
<div className="w-full">
<VoiceRecorder
targetText={currentCard.example}
targetTranslation={currentCard.exampleTranslation}
exampleImage={currentCard.exampleImage}
instructionText="請看例句圖片並大聲說出完整的例句:"
onRecordingComplete={() => {
// 簡化處理:直接顯示結果
handleSpeakingAnswer(currentCard.example)
}}
/>
</div>
{showResult && (
<div className="mt-6 p-6 rounded-lg bg-blue-50 border border-blue-200 w-full">
<p className="text-blue-700 text-left text-xl font-semibold mb-2">
</p>
<p className="text-gray-600 text-left">
...
</p>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === cards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-listening' ? (
/* Sentence Listening Test Mode - 例句聽力題 */
<div className="relative">
{/* Error Report Button */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficulty}
</span>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
<div className="text-center mb-8">
<div className="mb-6">
<AudioPlayer text={currentCard.example} />
<p className="text-sm text-gray-500 mt-2">
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-3 mb-6">
{/* 這裡需要例句選項 */}
<div className="text-center text-gray-500">
[...]
</div>
</div>
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === cards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : mode === 'sentence-reorder' ? (
/* Sentence Reorder Mode - 例句重組題 */
<div className="relative">
{/* Error Report Button */}
<div className="flex justify-end mb-2">
<button
onClick={() => {
setReportingCard(currentCard)
setShowReportModal(true)
}}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* Title in top-left */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficulty}
</span>
</div>
{/* Example Image */}
{currentCard.exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={currentCard.exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(currentCard.exampleImage)}
/>
</div>
</div>
)}
{/* Arranged Sentence Area */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left"></h3>
<div className="relative min-h-[120px] bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-300">
{arrangedWords.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center text-gray-400 text-lg">
</div>
) : (
<div className="flex flex-wrap gap-2">
{arrangedWords.map((word, index) => (
<div
key={`arranged-${index}`}
className="inline-flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-blue-200 transition-colors"
onClick={() => handleRemoveFromArranged(word)}
>
{word}
<span className="ml-2 text-blue-600 hover:text-blue-800">×</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Instructions Test Action */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* Shuffled Words */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left"></h3>
<div className="bg-white border border-gray-200 rounded-lg p-4 min-h-[80px]">
{shuffledWords.length === 0 ? (
<div className="text-center text-gray-400">
使
</div>
) : (
<div className="flex flex-wrap gap-2">
{shuffledWords.map((word, index) => (
<button
key={`shuffled-${index}`}
onClick={() => handleWordClick(word)}
className="bg-gray-100 text-gray-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-gray-200 active:bg-gray-300 transition-colors select-none"
>
{word}
</button>
))}
</div>
)}
</div>
</div>
{/* Control Buttons */}
<div className="flex gap-3 mb-6">
{arrangedWords.length > 0 && (
<button
onClick={handleCheckReorderAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
</button>
)}
<button
onClick={handleResetReorder}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
</button>
</div>
{/* Result Feedback */}
{reorderResult !== null && (
<div className={`p-6 rounded-lg w-full mb-6 ${
reorderResult
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
reorderResult ? 'text-green-700' : 'text-red-700'
}`}>
{reorderResult ? '正確!' : '錯誤!'}
</p>
{!reorderResult && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">"{currentCard.example}"</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
<strong></strong>{currentCard.exampleTranslation}
</p>
</div>
</div>
</div>
)}
</div>
{/* Navigation */}
<div className="flex gap-4 mt-6">
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
</button>
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex === cards.length - 1 ? '完成' : '下一張'}
</button>
</div>
</div>
) : null}
{/* Report Modal */}
{showReportModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="mb-4">
<p className="text-sm text-gray-600 mb-2">
{reportingCard?.word}
</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<select
value={reportReason}
onChange={(e) => setReportReason(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
<option value=""></option>
<option value="translation"></option>
<option value="definition"></option>
<option value="pronunciation"></option>
<option value="example"></option>
<option value="image"></option>
<option value="other"></option>
</select>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowReportModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50"
>
</button>
<button
onClick={handleReportSubmit}
disabled={!reportReason}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
</div>
)}
{/* Image Modal */}
{modalImage && (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
onClick={() => setModalImage(null)}
>
<div className="relative max-w-4xl max-h-[90vh] mx-4">
<img
src={modalImage}
alt="放大圖片"
className="max-w-full max-h-full rounded-lg"
/>
<button
onClick={() => setModalImage(null)}
className="absolute top-4 right-4 bg-black bg-opacity-50 text-white p-2 rounded-full hover:bg-opacity-75"
>
</button>
</div>
</div>
)}
{/* Complete Modal */}
{showComplete && (
<LearningComplete
score={score}
mode={mode}
onRestart={handleRestart}
onBackToDashboard={() => router.push('/dashboard')}
/>
)}
</div>
<style jsx>{`
.card-container {
perspective: 1000px;
transition: height 0.3s ease;
overflow: visible;
position: relative;
}
.card {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s ease;
transform-style: preserve-3d;
}
.card.flipped {
transform: rotateY(180deg);
}
.card-front, .card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: stretch;
justify-content: center;
top: 0;
left: 0;
}
.card-back {
transform: rotateY(180deg);
}
.card-front > div {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: 2rem;
box-sizing: border-box;
}
.card-back > div {
width: 100%;
padding: 1.5rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
`}</style>
</div>
)
}