diff --git a/frontend/app/review-design/page.tsx b/frontend/app/review-design/page.tsx index 9a60f48..4258217 100644 --- a/frontend/app/review-design/page.tsx +++ b/frontend/app/review-design/page.tsx @@ -61,6 +61,7 @@ export default function ReviewTestsPage() { filledQuestionText: undefined, exampleTranslation: "載入中...", pronunciation: "", + synonyms: [], difficultyLevel: "A1", translation: "載入中", exampleImage: undefined diff --git a/frontend/components/review/review-tests/FlipMemoryTest.tsx b/frontend/components/review/review-tests/FlipMemoryTest.tsx index f7e17e9..22e9f9f 100644 --- a/frontend/components/review/review-tests/FlipMemoryTest.tsx +++ b/frontend/components/review/review-tests/FlipMemoryTest.tsx @@ -2,15 +2,12 @@ import { useState, useRef, useEffect, memo, useCallback } from 'react' import AudioPlayer from '@/components/AudioPlayer' import { ErrorReportButton, - ConfidenceButtons, TestHeader, - HintPanel + ConfidenceLevel } from '@/components/review/shared' import { ConfidenceTestProps } from '@/types/review' -interface FlipMemoryTestProps extends ConfidenceTestProps { - // FlipMemoryTest specific props (if any) -} +interface FlipMemoryTestProps extends ConfidenceTestProps {} const FlipMemoryTestComponent: React.FC = ({ cardData, @@ -24,6 +21,9 @@ const FlipMemoryTestComponent: React.FC = ({ const frontRef = useRef(null) const backRef = useRef(null) + // 判斷是否已答題(選擇了信心等級) + const hasAnswered = selectedConfidence !== null + useEffect(() => { const updateCardHeight = () => { if (backRef.current) { @@ -54,11 +54,10 @@ const FlipMemoryTestComponent: React.FC = ({ }, [disabled, isFlipped]) const handleConfidenceSelect = useCallback((level: number) => { - if (disabled) return + if (disabled || hasAnswered) return setSelectedConfidence(level) onConfidenceSubmit(level) - }, [disabled, onConfidenceSubmit]) - + }, [disabled, hasAnswered, onConfidenceSubmit]) return (
@@ -68,18 +67,29 @@ const FlipMemoryTestComponent: React.FC = ({ {/* 翻卡容器 */}
{/* 正面 */}
@@ -115,8 +125,11 @@ const FlipMemoryTestComponent: React.FC = ({ {/* 背面 */}
@@ -161,75 +174,20 @@ const FlipMemoryTestComponent: React.FC = ({
)} -
- {/* 信心等級評估區 */} + {/* 信心等級評估區 - 使用新元件 */}
-
- -
) } diff --git a/frontend/components/review/review-tests/SentenceFillTest.tsx b/frontend/components/review/review-tests/SentenceFillTest.tsx index 884f019..e0cd25d 100644 --- a/frontend/components/review/review-tests/SentenceFillTest.tsx +++ b/frontend/components/review/review-tests/SentenceFillTest.tsx @@ -1,16 +1,14 @@ import React, { useState, useMemo, useCallback, memo } from 'react' import { getCorrectAnswer } from '@/utils/answerExtractor' import { - ErrorReportButton, - SentenceInput, TestResultDisplay, - HintPanel + HintPanel, + FillTestContainer, + TextInput } from '@/components/review/shared' import { FillTestProps } from '@/types/review' -interface SentenceFillTestProps extends FillTestProps { - // SentenceFillTest specific props (if any) -} +interface SentenceFillTestProps extends FillTestProps {} const SentenceFillTestComponent: React.FC = ({ cardData, @@ -22,11 +20,12 @@ const SentenceFillTestComponent: React.FC = ({ const [showResult, setShowResult] = useState(false) const [showHint, setShowHint] = useState(false) - const handleSubmit = useCallback(() => { - if (disabled || showResult || !fillAnswer.trim()) return + + const handleSubmit = useCallback((answer: string) => { + if (disabled || showResult) return setShowResult(true) - onAnswer(fillAnswer) - }, [disabled, showResult, fillAnswer, onAnswer]) + onAnswer(answer) + }, [disabled, showResult, onAnswer]) const handleToggleHint = useCallback(() => { setShowHint(prev => !prev) @@ -55,14 +54,18 @@ const SentenceFillTestComponent: React.FC = ({ {part} {index < parts.length - 1 && ( - + + + )} ))} @@ -79,14 +82,18 @@ const SentenceFillTestComponent: React.FC = ({ {part} {index < matches.length && ( - + + + )} ))} @@ -101,95 +108,98 @@ const SentenceFillTestComponent: React.FC = ({ handleSubmit, disabled, showResult, - correctAnswer.length + isCorrect, + correctAnswer ]) - return ( -
-
- -
- -
- {/* 標題區 */} -
-

例句填空

- - {cardData.difficultyLevel} - + // 句子顯示區域 + const sentenceArea = ( +
+ {/* 圖片區(如果有) */} + {cardData.exampleImage && ( +
+ Example illustration { + // 圖片點擊處理 - 後續可以添加放大功能 + }} + />
+ )} - {/* 圖片區(如果有) */} - {cardData.exampleImage && ( -
-
- Example illustration { - // 這裡需要處理圖片點擊,但我們暫時移除 onImageClick - // 因為新的 cardData 接口可能不包含這個功能 - }} - /> -
-
- )} + {/* 指示文字 */} +

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

- {/* 指示文字 */} -

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

- - {/* 填空句子區域 */} -
-
- {renderFilledSentence()} -
-
- - {/* 操作按鈕 */} -
- - -
- - {/* 提示區域 */} - - - {/* 結果反饋區 */} - + {/* 填空句子區域 */} +
+ {renderFilledSentence()}
) + + // 輸入區域(包含操作按鈕和提示) + const inputArea = ( +
+ {/* 操作按鈕 */} +
+ + +
+ + {/* 提示區域 */} + +
+ ) + + // 結果顯示區域 + const resultArea = showResult ? ( + + ) : null + + return ( + + ) } export const SentenceFillTest = memo(SentenceFillTestComponent) diff --git a/frontend/components/review/review-tests/SentenceListeningTest.tsx b/frontend/components/review/review-tests/SentenceListeningTest.tsx index ef701e6..26fc891 100644 --- a/frontend/components/review/review-tests/SentenceListeningTest.tsx +++ b/frontend/components/review/review-tests/SentenceListeningTest.tsx @@ -1,9 +1,9 @@ import React, { useState, useCallback, useMemo, memo } from 'react' import AudioPlayer from '@/components/AudioPlayer' import { - ErrorReportButton, TestResultDisplay, - TestHeader + ListeningTestContainer, + ChoiceGrid } from '@/components/review/shared' import { ChoiceTestProps } from '@/types/review' @@ -24,6 +24,9 @@ const SentenceListeningTestComponent: React.FC = ({ const [selectedAnswer, setSelectedAnswer] = useState(null) const [showResult, setShowResult] = useState(false) + // 判斷是否已答題(選擇了答案) + const hasAnswered = selectedAnswer !== null + const handleAnswerSelect = useCallback((answer: string) => { if (disabled || showResult) return setSelectedAnswer(answer) @@ -33,84 +36,73 @@ const SentenceListeningTestComponent: React.FC = ({ const isCorrect = useMemo(() => selectedAnswer === cardData.example, [selectedAnswer, cardData.example]) - return ( -
- {/* 錯誤回報按鈕 */} -
- -
- -
- {/* 標題區 */} - - - {/* 指示文字 */} -

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

- - {/* 音頻播放區 */} -
-
- -
-
- - {/* 圖片區(如果有) */} - {exampleImage && ( -
-
- Example illustration onImageClick?.(exampleImage)} - /> -
-
- )} - - {/* 選項區域 - 響應式網格布局 */} -
- {options.map((sentence, idx) => ( - - ))} -
- - {/* 結果反饋區 */} - {showResult && ( - - )} -
+ // 音頻播放區域 + const audioArea = ( +
+
) + + // 問題顯示區域(包含圖片) + const questionArea = ( +
+

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

+ + {/* 圖片區(如果有) */} + {exampleImage && ( +
+ Example illustration onImageClick?.(exampleImage)} + /> +
+ )} +
+ ) + + // 答題區域 + const answerArea = ( + + ) + + // 結果顯示區域 + const resultArea = showResult ? ( + + ) : null + + return ( + + ) } export const SentenceListeningTest = memo(SentenceListeningTestComponent) diff --git a/frontend/components/review/review-tests/SentenceReorderTest.tsx b/frontend/components/review/review-tests/SentenceReorderTest.tsx index 1c5d8f9..d4612c1 100644 --- a/frontend/components/review/review-tests/SentenceReorderTest.tsx +++ b/frontend/components/review/review-tests/SentenceReorderTest.tsx @@ -1,10 +1,8 @@ -import React, { useState, useEffect, useCallback, useMemo, memo } from 'react' -import AudioPlayer from '@/components/AudioPlayer' +import React, { useState, useEffect, useCallback, memo } from 'react' import { ReorderTestProps } from '@/types/review' import { - ErrorReportButton, TestResultDisplay, - TestHeader + TestContainer } from '@/components/review/shared' interface SentenceReorderTestProps extends ReorderTestProps { @@ -25,6 +23,9 @@ const SentenceReorderTestComponent: React.FC = ({ const [showResult, setShowResult] = useState(false) const [reorderResult, setReorderResult] = useState(null) + // 判斷是否已答題(完成重組後設定 hasAnswered = true) + const hasAnswered = showResult + // 初始化單字順序 useEffect(() => { const words = cardData.example.split(/\s+/).filter(word => word.length > 0) @@ -63,126 +64,129 @@ const SentenceReorderTestComponent: React.FC = ({ setReorderResult(null) }, [disabled, showResult, cardData.example]) - return ( -
-
- -
- -
- {/* 標題區 */} -
-

例句重組

- - {cardData.difficultyLevel} - -
- - {/* 圖片區(如果有) */} - {exampleImage && ( -
-
- Example illustration onImageClick?.(exampleImage)} - /> -
-
- )} - - {/* 重組區域 */} -
-

重組區域:

-
- {arrangedWords.length === 0 ? ( -
- 請嘗試組成完整句子 -
- ) : ( -
- {arrangedWords.map((word, index) => ( -
handleRemoveFromArranged(word)} - > - {word} - × -
- ))} -
- )} -
-
- - {/* 指示文字 */} -

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

- - {/* 可用單字區域 */} -
-

可用單字:

-
- {shuffledWords.length === 0 ? ( -
- 所有單字都已使用 -
- ) : ( -
- {shuffledWords.map((word, index) => ( - - ))} -
- )} -
-
- - {/* 控制按鈕 */} -
- {arrangedWords.length > 0 && !showResult && ( - - )} - -
- - {/* 結果反饋區 */} - {showResult && reorderResult !== null && ( - + {/* 圖片區(如果有) */} + {exampleImage && ( +
+ Example illustration onImageClick?.(exampleImage)} /> - )} +
+ )} + + {/* 指示文字 */} +

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

+ + {/* 重組區域 */} +
+

重組區域:

+
+ {arrangedWords.length === 0 ? ( +
+ 請嘗試組成完整句子 +
+ ) : ( +
+ {arrangedWords.map((word, index) => ( +
handleRemoveFromArranged(word)} + > + {word} + × +
+ ))} +
+ )} +
) + + // 答題區域 + const answerArea = ( +
+ {/* 可用單字區域 */} +
+

可用單字:

+
+ {shuffledWords.length === 0 ? ( +
+ 所有單字都已使用 +
+ ) : ( +
+ {shuffledWords.map((word, index) => ( + + ))} +
+ )} +
+
+ + {/* 控制按鈕 */} +
+ {arrangedWords.length > 0 && !showResult && ( + + )} + +
+
+ ) + + // 結果顯示區域 + const resultArea = showResult && reorderResult !== null ? ( + + ) : null + + return ( + + ) } export const SentenceReorderTest = memo(SentenceReorderTestComponent) diff --git a/frontend/components/review/review-tests/SentenceSpeakingTest.tsx b/frontend/components/review/review-tests/SentenceSpeakingTest.tsx index 2234108..d3af36d 100644 --- a/frontend/components/review/review-tests/SentenceSpeakingTest.tsx +++ b/frontend/components/review/review-tests/SentenceSpeakingTest.tsx @@ -1,8 +1,7 @@ import React, { useState, useCallback, memo } from 'react' import VoiceRecorder from '@/components/VoiceRecorder' import { - ErrorReportButton, - TestHeader + SpeakingTestContainer } from '@/components/review/shared' import { BaseReviewProps } from '@/types/review' @@ -21,51 +20,71 @@ const SentenceSpeakingTestComponent: React.FC = ({ }) => { const [showResult, setShowResult] = useState(false) + // 判斷是否已答題(錄音提交後設定 hasAnswered = true) + const hasAnswered = showResult + const handleRecordingComplete = useCallback(() => { if (disabled || showResult) return setShowResult(true) onAnswer(cardData.example) // 語音測驗通常都算正確 }, [disabled, showResult, cardData.example, onAnswer]) - return ( -
- {/* 錯誤回報按鈕 */} -
- -
- -
- {/* 標題區 */} - - - {/* VoiceRecorder 組件區域 */} -
- +

+ 請看例句圖片並大聲說出完整的例句: +

+ {exampleImage && ( +
+ Example illustration onImageClick?.(exampleImage)} />
- - {/* 結果反饋區 */} - {showResult && ( -
-

- 錄音完成! -

-

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

-
- )} -
+ )}
) + + // 錄音區域 + const recordingArea = ( +
+ +
+ ) + + // 結果顯示區域 + const resultArea = showResult ? ( +
+

+ 錄音完成! +

+

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

+
+ ) : null + + return ( + + ) } export const SentenceSpeakingTest = memo(SentenceSpeakingTestComponent) diff --git a/frontend/components/review/review-tests/VocabChoiceTest.tsx b/frontend/components/review/review-tests/VocabChoiceTest.tsx index 0676fde..d9e6112 100644 --- a/frontend/components/review/review-tests/VocabChoiceTest.tsx +++ b/frontend/components/review/review-tests/VocabChoiceTest.tsx @@ -1,15 +1,12 @@ import React, { useState, useCallback, useMemo, memo } from 'react' -import AudioPlayer from '@/components/AudioPlayer' import { ChoiceTestProps } from '@/types/review' import { - ErrorReportButton, TestResultDisplay, - TestHeader + ChoiceTestContainer, + ChoiceGrid } from '@/components/review/shared' -interface VocabChoiceTestProps extends ChoiceTestProps { - // VocabChoiceTest specific props (if any) -} +interface VocabChoiceTestProps extends ChoiceTestProps {} const VocabChoiceTestComponent: React.FC = ({ cardData, @@ -21,6 +18,7 @@ const VocabChoiceTestComponent: React.FC = ({ const [selectedAnswer, setSelectedAnswer] = useState(null) const [showResult, setShowResult] = useState(false) + const handleAnswerSelect = useCallback((answer: string) => { if (disabled || showResult) return setSelectedAnswer(answer) @@ -32,70 +30,57 @@ const VocabChoiceTestComponent: React.FC = ({ selectedAnswer === cardData.word , [selectedAnswer, cardData.word]) - return ( -
-
- -
- -
- {/* 標題區 */} - - - {/* 指示文字 */} -

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

- - {/* 定義顯示區 */} -
-
-

定義

-

{cardData.definition}

-
-
- - {/* 選項區域 - 響應式網格布局 */} -
- {options.map((option, idx) => ( - - ))} -
- - {/* 結果反饋區 */} - {showResult && ( - - )} + // 問題顯示區域 + const questionArea = ( +
+
+

定義

+

{cardData.definition}

+

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

) + + // 選項區域 + const optionsArea = ( + + ) + + // 結果顯示區域 + const resultArea = showResult ? ( + + ) : null + + return ( + + ) } export const VocabChoiceTest = memo(VocabChoiceTestComponent) diff --git a/frontend/components/review/review-tests/VocabListeningTest.tsx b/frontend/components/review/review-tests/VocabListeningTest.tsx index f3041bd..16f354d 100644 --- a/frontend/components/review/review-tests/VocabListeningTest.tsx +++ b/frontend/components/review/review-tests/VocabListeningTest.tsx @@ -1,15 +1,13 @@ import React, { useState, useCallback, useMemo, memo } from 'react' import AudioPlayer from '@/components/AudioPlayer' import { - ErrorReportButton, TestResultDisplay, - TestHeader + ListeningTestContainer, + ChoiceGrid } from '@/components/review/shared' import { ChoiceTestProps } from '@/types/review' -interface VocabListeningTestProps extends ChoiceTestProps { - // VocabListeningTest specific props (if any) -} +interface VocabListeningTestProps extends ChoiceTestProps {} const VocabListeningTestComponent: React.FC = ({ cardData, @@ -21,6 +19,9 @@ const VocabListeningTestComponent: React.FC = ({ const [selectedAnswer, setSelectedAnswer] = useState(null) const [showResult, setShowResult] = useState(false) + // 判斷是否已答題(選擇了答案) + const hasAnswered = selectedAnswer !== null + const handleAnswerSelect = useCallback((answer: string) => { if (disabled || showResult) return setSelectedAnswer(answer) @@ -30,74 +31,63 @@ const VocabListeningTestComponent: React.FC = ({ const isCorrect = useMemo(() => selectedAnswer === cardData.word, [selectedAnswer, cardData.word]) - return ( -
- {/* 錯誤回報按鈕 */} -
- -
- -
- {/* 標題區 */} - - - {/* 指示文字 */} -

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

- - {/* 音頻播放區 */} -
-
-

發音

-
- {cardData.pronunciation && {cardData.pronunciation}} - -
-
-
- - {/* 選項區域 - 2x2網格布局 */} -
- {options.map((option) => ( - - ))} -
- - {/* 結果反饋區 */} - {showResult && ( - - )} + // 音頻播放區域 + const audioArea = ( +
+

發音

+
+ {cardData.pronunciation && {cardData.pronunciation}} +
) + + // 問題顯示區域 + const questionArea = ( +

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

+ ) + + // 答題區域 + const answerArea = ( + + ) + + // 結果顯示區域 + const resultArea = showResult ? ( + + ) : null + + return ( + + ) } export const VocabListeningTest = memo(VocabListeningTestComponent) diff --git a/frontend/components/review/shared/BaseTestComponent.tsx b/frontend/components/review/shared/BaseTestComponent.tsx index 31d776c..efb3ac5 100644 --- a/frontend/components/review/shared/BaseTestComponent.tsx +++ b/frontend/components/review/shared/BaseTestComponent.tsx @@ -39,19 +39,6 @@ export const BaseTestComponent: React.FC = ({ showResult: false }) - // 更新測驗狀態 - const updateTestState = useCallback((updates: Partial) => { - setTestState(prev => ({ ...prev, ...updates })) - }, []) - - // 提供給子元件的狀態和方法 - const testContext = { - testState, - updateTestState, - cardData, - disabled: disabled || testState.showResult - } - return (
{/* 錯誤回報按鈕 */} @@ -76,7 +63,7 @@ export const BaseTestComponent: React.FC = ({ {/* 測驗內容區域 */}
- {React.cloneElement(children as React.ReactElement, { testContext })} + {children}
{/* 結果顯示區域 */} diff --git a/frontend/components/review/shared/TestContainer.tsx b/frontend/components/review/shared/TestContainer.tsx index 29e5179..276c1ce 100644 --- a/frontend/components/review/shared/TestContainer.tsx +++ b/frontend/components/review/shared/TestContainer.tsx @@ -92,7 +92,7 @@ export const TestContainer: React.FC = ({ */ // 選擇題容器 -export interface ChoiceTestContainerProps extends Omit { +export interface ChoiceTestContainerProps extends Omit { questionArea: ReactNode optionsArea: ReactNode } @@ -125,7 +125,7 @@ export const ChoiceTestContainer: React.FC = ({ } // 填空題容器 -export interface FillTestContainerProps extends Omit { +export interface FillTestContainerProps extends Omit { sentenceArea: ReactNode inputArea: ReactNode } @@ -150,7 +150,7 @@ export const FillTestContainer: React.FC = ({ } // 聽力測驗容器 -export interface ListeningTestContainerProps extends Omit { +export interface ListeningTestContainerProps extends Omit { audioArea: ReactNode questionArea: ReactNode answerArea: ReactNode @@ -186,7 +186,7 @@ export const ListeningTestContainer: React.FC = ({ } // 口說測驗容器 -export interface SpeakingTestContainerProps extends Omit { +export interface SpeakingTestContainerProps extends Omit { promptArea: ReactNode recordingArea: ReactNode }