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

223 lines
8.1 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 { 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">
<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>
)}
{/* 重組區域 */}
<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>
)
}