'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(null) const [showResult, setShowResult] = useState(false) const [fillAnswer, setFillAnswer] = useState('') const [showHint, setShowHint] = useState(false) const [modalImage, setModalImage] = useState(null) const [showReportModal, setShowReportModal] = useState(false) const [reportReason, setReportReason] = useState('') const [reportingCard, setReportingCard] = useState(null) const [showComplete, setShowComplete] = useState(false) const [quizOptions, setQuizOptions] = useState(['brought', 'determine', 'achieve', 'consider']) const [cardHeight, setCardHeight] = useState(400) // Sentence reorder states const [shuffledWords, setShuffledWords] = useState([]) const [arrangedWords, setArrangedWords] = useState([]) const [reorderResult, setReorderResult] = useState(null) // Refs for measuring card content heights const cardFrontRef = useRef(null) const cardBackRef = useRef(null) const cardContainerRef = useRef(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 (
載入中...
) } return (
{/* Navigation */} router.push('/dashboard')} />
{/* Progress Bar */}
進度
{currentCardIndex + 1} / {cards.length}
{score.correct} / {score.total} {score.total > 0 && ( ({Math.round((score.correct / score.total) * 100)}%) )}
{/* Mode Toggle */}
{mode === 'flip-memory' ? ( /* Flip Card Mode */
{/* Error Report Button for Flip Mode */}
{/* Front */}
{/* Title and Instructions */}

翻卡記憶

{currentCard.difficulty}

點擊卡片翻面,根據你對單字的熟悉程度進行自我評估:

{/* Word Display */}

{currentCard.word}

{currentCard.pronunciation}
{/* Back */}
{/* Content Sections */}
{/* Definition */}

定義

{currentCard.definition}

{/* Example */}

例句

"{currentCard.example}"

"{currentCard.exampleTranslation}"

{/* Synonyms */}

同義詞

{currentCard.synonyms.map((synonym, index) => ( {synonym} ))}
{/* Navigation */}
) : mode === 'vocab-choice' ? ( /* Vocab Choice Mode - 詞彙選擇 */
{/* Error Report Button for Quiz Mode */}
{/* Title in top-left */}

詞彙選擇

{currentCard.difficulty}

請選擇符合上述定義的英文詞彙:

定義

{currentCard.definition}

{quizOptions.map((option, idx) => ( ))}
{showResult && (

{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}

{selectedAnswer !== currentCard.word && (

正確答案是:{currentCard.word} {currentCard.pronunciation}

)} {selectedAnswer === currentCard.word && (

{currentCard.word} {currentCard.pronunciation}

)}
)}
{/* Navigation */}
) : mode === 'sentence-fill' ? ( /* Fill in the Blank Mode - 填空題 */
{/* Error Report Button for Fill Mode */}
{/* Title in top-left */}

例句填空

{currentCard.difficulty}
{/* Example Image */} {currentCard.exampleImage && (
Example illustration setModalImage(currentCard.exampleImage)} />
)}

請點擊例句中的空白處輸入正確的單字:

{/* Example Sentence with Blanks */}
{currentCard.example.split(new RegExp(`(${currentCard.word})`, 'gi')).map((part, index) => { const isTargetWord = part.toLowerCase() === currentCard.word.toLowerCase(); return isTargetWord ? ( 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 && ( ____ )} ) : ( {part} ); })}
{/* Action Buttons */}
{!showResult && fillAnswer.trim() && ( )}
{/* Hint Section */} {showHint && (

詞彙定義:

{currentCard.definition}

)} {showResult && (

{fillAnswer.toLowerCase().trim() === currentCard.word.toLowerCase() ? '正確!' : '錯誤!'}

{fillAnswer.toLowerCase().trim() !== currentCard.word.toLowerCase() && (

正確答案是:{currentCard.word}

)}

{currentCard.pronunciation}

)}
{/* Navigation */}
) : mode === 'vocab-listening' ? ( /* Listening Test Mode - 聽力測試 */
{/* Error Report Button for Listening Mode */}
{/* Title in top-left */}

詞彙聽力

{currentCard.difficulty}

請聽發音並選擇正確的英文單字:

{/* Content Sections */}
{/* Audio */}

發音

{currentCard.pronunciation}
{/* Word Options */}
{[currentCard.word, 'determine', 'achieve', 'consider'].map((word) => ( ))}
{showResult && (

{selectedAnswer === currentCard.word ? '正確!' : '錯誤!'}

{selectedAnswer !== currentCard.word && (

正確答案是:{currentCard.word}

發音:{currentCard.pronunciation}

)}
)}
{/* Navigation */}
) : mode === 'sentence-speaking' ? ( /* Speaking Test Mode - 口說測試 */
{/* Error Report Button for Speaking Mode */}
{/* Title in top-left */}

例句口說

{currentCard.difficulty}
{ // 簡化處理:直接顯示結果 handleSpeakingAnswer(currentCard.example) }} />
{showResult && (

錄音完成!

系統正在評估你的發音...

)}
{/* Navigation */}
) : mode === 'sentence-listening' ? ( /* Sentence Listening Test Mode - 例句聽力題 */
{/* Error Report Button */}
{/* Title in top-left */}

例句聽力

{currentCard.difficulty}

請聽例句並選擇正確的選項:

點擊播放聽例句

{/* 這裡需要例句選項 */}
[例句聽力題功能開發中...]
{/* Navigation */}
) : mode === 'sentence-reorder' ? ( /* Sentence Reorder Mode - 例句重組題 */
{/* Error Report Button */}
{/* Title in top-left */}

例句重組

{currentCard.difficulty}
{/* Example Image */} {currentCard.exampleImage && (
Example illustration setModalImage(currentCard.exampleImage)} />
)}

點擊下方單字,依序重組成正確的句子:

{/* Arranged Sentence Area */}

重組區域:

{arrangedWords.length === 0 ? (
點擊下方單字來重組句子
) : (
{arrangedWords.map((word, index) => (
handleRemoveFromArranged(word)} > {word} ×
))}
)}
{/* Shuffled Words */}

可用單字:

{shuffledWords.length === 0 ? (
所有單字都已使用
) : (
{shuffledWords.map((word, index) => ( ))}
)}
{/* Control Buttons */}
{arrangedWords.length > 0 && ( )}
{/* Result Feedback */} {reorderResult !== null && (

{reorderResult ? '正確!' : '錯誤!'}

{!reorderResult && (

正確答案是:"{currentCard.example}"

)}

中文翻譯:{currentCard.exampleTranslation}

)}
{/* Navigation */}
) : null} {/* Report Modal */} {showReportModal && (

回報錯誤

單字:{reportingCard?.word}

)} {/* Image Modal */} {modalImage && (
setModalImage(null)} >
放大圖片
)} {/* Complete Modal */} {showComplete && ( router.push('/dashboard')} /> )}
) }