dramaling-vocab-learning/frontend/components/review/review-tests/SentenceReorderTest.tsx

193 lines
6.9 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.

import React, { useState, useEffect, useCallback, memo } from 'react'
import { ReorderTestProps } from '@/types/review'
import {
TestResultDisplay,
TestContainer
} from '@/components/review/shared'
interface SentenceReorderTestProps extends ReorderTestProps {
exampleImage?: string
onImageClick?: (image: string) => void
}
const SentenceReorderTestComponent: React.FC<SentenceReorderTestProps> = ({
cardData,
exampleImage,
onAnswer,
onReportError,
onImageClick,
disabled = false
}) => {
const [shuffledWords, setShuffledWords] = useState<string[]>([])
const [arrangedWords, setArrangedWords] = useState<string[]>([])
const [showResult, setShowResult] = useState(false)
const [reorderResult, setReorderResult] = useState<boolean | null>(null)
// 判斷是否已答題(完成重組後設定 hasAnswered = true
const hasAnswered = showResult
// 初始化單字順序
useEffect(() => {
const words = cardData.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
}, [cardData.example])
const handleWordClick = useCallback((word: string) => {
if (disabled || showResult) return
setShuffledWords(prev => prev.filter(w => w !== word))
setArrangedWords(prev => [...prev, word])
}, [disabled, showResult])
const handleRemoveFromArranged = useCallback((word: string) => {
if (disabled || showResult) return
setArrangedWords(prev => prev.filter(w => w !== word))
setShuffledWords(prev => [...prev, word])
}, [disabled, showResult])
const handleCheckAnswer = useCallback(() => {
if (disabled || showResult || arrangedWords.length === 0) return
const userSentence = arrangedWords.join(' ')
const isCorrect = userSentence.toLowerCase().trim() === cardData.example.toLowerCase().trim()
setReorderResult(isCorrect)
setShowResult(true)
onAnswer(userSentence)
}, [disabled, showResult, arrangedWords, cardData.example, onAnswer])
const handleReset = useCallback(() => {
if (disabled || showResult) return
const words = cardData.example.split(/\s+/).filter(word => word.length > 0)
const shuffled = [...words].sort(() => Math.random() - 0.5)
setShuffledWords(shuffled)
setArrangedWords([])
setReorderResult(null)
}, [disabled, showResult, cardData.example])
// 主要內容區域
const contentArea = (
<div className="space-y-6">
{/* 圖片區(如果有) */}
{exampleImage && (
<div className="bg-gray-50 rounded-lg p-4">
<img
src={exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => onImageClick?.(exampleImage)}
/>
</div>
)}
{/* 指示文字 */}
<p className="text-lg text-gray-700 text-left">
</p>
{/* 重組區域 */}
<div>
<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>
</div>
)
// 答題區域
const answerArea = (
<div className="space-y-4">
{/* 可用單字區域 */}
<div>
<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)}
disabled={disabled || showResult}
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 disabled:opacity-50 disabled:cursor-not-allowed"
>
{word}
</button>
))}
</div>
)}
</div>
</div>
{/* 控制按鈕 */}
<div className="flex gap-3">
{arrangedWords.length > 0 && !showResult && (
<button
onClick={handleCheckAnswer}
disabled={disabled}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
)}
<button
onClick={handleReset}
disabled={disabled || showResult}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
)
// 結果顯示區域
const resultArea = showResult && reorderResult !== null ? (
<TestResultDisplay
isCorrect={reorderResult}
correctAnswer={cardData.example}
userAnswer={arrangedWords.join(' ')}
word={cardData.word}
pronunciation={cardData.pronunciation}
example={cardData.example}
exampleTranslation={cardData.exampleTranslation}
showResult={showResult}
/>
) : null
return (
<TestContainer
cardData={cardData}
testTitle="例句重組"
contentArea={contentArea}
answerArea={answerArea}
resultArea={resultArea}
onAnswer={onAnswer}
onReportError={onReportError}
disabled={disabled}
/>
)
}
export const SentenceReorderTest = memo(SentenceReorderTestComponent)
SentenceReorderTest.displayName = 'SentenceReorderTest'