222 lines
8.0 KiB
TypeScript
222 lines
8.0 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import AudioPlayer from '@/components/AudioPlayer'
|
||
|
||
interface SentenceReorderTestProps {
|
||
word: string
|
||
definition: string
|
||
example: string
|
||
exampleTranslation: string
|
||
difficultyLevel: string
|
||
exampleImage?: string
|
||
onAnswer: (answer: string) => void
|
||
onReportError: () => void
|
||
onImageClick?: (image: string) => void
|
||
disabled?: boolean
|
||
}
|
||
|
||
export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
||
word,
|
||
definition,
|
||
example,
|
||
exampleTranslation,
|
||
difficultyLevel,
|
||
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)
|
||
|
||
// 初始化單字順序
|
||
useEffect(() => {
|
||
const words = example.split(/\s+/).filter(word => word.length > 0)
|
||
const shuffled = [...words].sort(() => Math.random() - 0.5)
|
||
setShuffledWords(shuffled)
|
||
setArrangedWords([])
|
||
}, [example])
|
||
|
||
const handleWordClick = (word: string) => {
|
||
if (disabled || showResult) return
|
||
setShuffledWords(prev => prev.filter(w => w !== word))
|
||
setArrangedWords(prev => [...prev, word])
|
||
}
|
||
|
||
const handleRemoveFromArranged = (word: string) => {
|
||
if (disabled || showResult) return
|
||
setArrangedWords(prev => prev.filter(w => w !== word))
|
||
setShuffledWords(prev => [...prev, word])
|
||
}
|
||
|
||
const handleCheckAnswer = () => {
|
||
if (disabled || showResult || arrangedWords.length === 0) return
|
||
const userSentence = arrangedWords.join(' ')
|
||
const isCorrect = userSentence.toLowerCase().trim() === example.toLowerCase().trim()
|
||
setReorderResult(isCorrect)
|
||
setShowResult(true)
|
||
onAnswer(userSentence)
|
||
}
|
||
|
||
const handleReset = () => {
|
||
if (disabled || showResult) return
|
||
const words = example.split(/\s+/).filter(word => word.length > 0)
|
||
const shuffled = [...words].sort(() => Math.random() - 0.5)
|
||
setShuffledWords(shuffled)
|
||
setArrangedWords([])
|
||
setReorderResult(null)
|
||
}
|
||
|
||
return (
|
||
<div className="relative">
|
||
{/* 錯誤回報按鈕 */}
|
||
<div className="flex justify-end mb-2">
|
||
<button
|
||
onClick={onReportError}
|
||
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">
|
||
{/* 標題區 */}
|
||
<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">
|
||
{difficultyLevel}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 圖片區(如果有) */}
|
||
{exampleImage && (
|
||
<div className="mb-6">
|
||
<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>
|
||
</div>
|
||
)}
|
||
|
||
{/* 重組區域 */}
|
||
<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>
|
||
|
||
{/* 指示文字 */}
|
||
<p className="text-lg text-gray-700 mb-6 text-left">
|
||
點擊下方單字,依序重組成正確的句子:
|
||
</p>
|
||
|
||
{/* 可用單字區域 */}
|
||
<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)}
|
||
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 mb-6">
|
||
{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>
|
||
|
||
{/* 結果反饋區 */}
|
||
{showResult && 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">"{example}"</strong>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-3">
|
||
<div className="text-left">
|
||
<div className="flex items-center text-gray-600">
|
||
<strong>發音:</strong>
|
||
<AudioPlayer text={example} />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="text-left">
|
||
<p className="text-gray-600">
|
||
<strong>中文翻譯:</strong>{exampleTranslation}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
} |