1370 lines
53 KiB
TypeScript
1370 lines
53 KiB
TypeScript
'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: '/ˈ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]
|
||
|
||
// 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>
|
||
)
|
||
} |