211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
import { useState } from 'react'
|
||
import AudioPlayer from '@/components/AudioPlayer'
|
||
|
||
interface SentenceFillTestProps {
|
||
word: string
|
||
definition: string
|
||
example: string
|
||
exampleTranslation: string
|
||
pronunciation?: string
|
||
difficultyLevel: string
|
||
exampleImage?: string
|
||
onAnswer: (answer: string) => void
|
||
onReportError: () => void
|
||
onImageClick?: (image: string) => void
|
||
disabled?: boolean
|
||
}
|
||
|
||
export const SentenceFillTest: React.FC<SentenceFillTestProps> = ({
|
||
word,
|
||
definition,
|
||
example,
|
||
exampleTranslation,
|
||
pronunciation,
|
||
difficultyLevel,
|
||
exampleImage,
|
||
onAnswer,
|
||
onReportError,
|
||
onImageClick,
|
||
disabled = false
|
||
}) => {
|
||
const [fillAnswer, setFillAnswer] = useState('')
|
||
const [showResult, setShowResult] = useState(false)
|
||
const [showHint, setShowHint] = useState(false)
|
||
|
||
const handleSubmit = () => {
|
||
if (disabled || showResult || !fillAnswer.trim()) return
|
||
setShowResult(true)
|
||
onAnswer(fillAnswer)
|
||
}
|
||
|
||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||
if (e.key === 'Enter' && !showResult && fillAnswer.trim()) {
|
||
handleSubmit()
|
||
}
|
||
}
|
||
|
||
const isCorrect = fillAnswer.toLowerCase().trim() === word.toLowerCase()
|
||
const targetWordLength = word.length
|
||
const inputWidth = Math.max(100, Math.max(targetWordLength * 12, fillAnswer.length * 12 + 20))
|
||
|
||
// 將例句中的目標詞替換為輸入框
|
||
const renderSentenceWithInput = () => {
|
||
const parts = example.split(new RegExp(`\\b${word}\\b`, 'gi'))
|
||
const matches = example.match(new RegExp(`\\b${word}\\b`, 'gi')) || []
|
||
|
||
return (
|
||
<div className="text-lg text-gray-700 leading-relaxed">
|
||
{parts.map((part, index) => (
|
||
<span key={index}>
|
||
{part}
|
||
{index < matches.length && (
|
||
<span className="relative inline-block mx-1">
|
||
<input
|
||
type="text"
|
||
value={fillAnswer}
|
||
onChange={(e) => setFillAnswer(e.target.value)}
|
||
onKeyDown={handleKeyDown}
|
||
placeholder=""
|
||
disabled={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: `${inputWidth}px` }}
|
||
/>
|
||
{!fillAnswer && (
|
||
<span
|
||
className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
|
||
style={{ paddingBottom: '8px' }}
|
||
>
|
||
____
|
||
</span>
|
||
)}
|
||
</span>
|
||
)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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">
|
||
<h3 className="font-semibold text-gray-900 mb-2 text-left">圖片提示</h3>
|
||
<img
|
||
src={exampleImage}
|
||
alt="Example illustration"
|
||
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
|
||
onClick={() => onImageClick?.(exampleImage)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 指示文字 */}
|
||
<p className="text-lg text-gray-700 mb-6 text-left">
|
||
請點擊例句中的空白處輸入正確的單字:
|
||
</p>
|
||
|
||
{/* 填空句子區域 */}
|
||
<div className="mb-6">
|
||
<div className="bg-gray-50 rounded-lg p-6">
|
||
{renderSentenceWithInput()}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按鈕 */}
|
||
<div className="flex gap-3 mb-4">
|
||
{!showResult && fillAnswer.trim() && (
|
||
<button
|
||
onClick={handleSubmit}
|
||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 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>
|
||
|
||
{/* 提示區域 */}
|
||
{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">{definition}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 結果反饋區 */}
|
||
{showResult && (
|
||
<div className={`mt-6 p-6 rounded-lg w-full ${
|
||
isCorrect
|
||
? 'bg-green-50 border border-green-200'
|
||
: 'bg-red-50 border border-red-200'
|
||
}`}>
|
||
<p className={`font-semibold text-left text-xl mb-4 ${
|
||
isCorrect ? 'text-green-700' : 'text-red-700'
|
||
}`}>
|
||
{isCorrect ? '正確!' : '錯誤!'}
|
||
</p>
|
||
|
||
{!isCorrect && (
|
||
<div className="mb-4">
|
||
<p className="text-gray-700 text-left">
|
||
正確答案是:<strong className="text-lg">{word}</strong>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-3">
|
||
<div className="text-left">
|
||
<p className="text-gray-600">
|
||
<strong>發音:</strong>
|
||
{pronunciation && <span className="mx-2">{pronunciation}</span>}
|
||
<AudioPlayer text={word} />
|
||
</p>
|
||
</div>
|
||
|
||
<div className="text-left">
|
||
<p className="text-gray-600">
|
||
<strong>完整例句:</strong>"{example}"
|
||
</p>
|
||
<p className="text-gray-500 text-sm">
|
||
<strong>翻譯:</strong>"{exampleTranslation}"
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
} |