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

192 lines
7.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, useRef, useEffect, memo, useCallback } from 'react'
import AudioPlayer from '@/components/media/AudioPlayer'
import {
ErrorReportButton,
TestHeader,
ConfidenceLevel
} from '@/components/review/shared'
import { ConfidenceTestProps } from '@/types/review'
interface FlipMemoryTestProps extends ConfidenceTestProps {}
const FlipMemoryTestComponent: React.FC<FlipMemoryTestProps> = ({
cardData,
onConfidenceSubmit,
onReportError,
disabled = false
}) => {
const [isFlipped, setIsFlipped] = useState(false)
const [selectedConfidence, setSelectedConfidence] = useState<number | null>(null)
const [cardHeight, setCardHeight] = useState<number>(400)
const frontRef = useRef<HTMLDivElement>(null)
const backRef = useRef<HTMLDivElement>(null)
// 判斷是否已答題(選擇了信心等級)
const hasAnswered = selectedConfidence !== null
useEffect(() => {
const updateCardHeight = () => {
if (backRef.current) {
const backHeight = backRef.current.scrollHeight
// 響應式最小高度設定
const minHeightByScreen = window.innerWidth <= 480 ? 300 :
window.innerWidth <= 768 ? 350 : 400
// 以背面內容高度為準,不設最大高度限制
const finalHeight = Math.max(minHeightByScreen, backHeight)
setCardHeight(finalHeight)
}
}
// 延遲執行以確保內容已渲染
const timer = setTimeout(updateCardHeight, 100)
window.addEventListener('resize', updateCardHeight)
return () => {
clearTimeout(timer)
window.removeEventListener('resize', updateCardHeight)
}
}, [cardData.word, cardData.definition, cardData.example, cardData.synonyms])
const handleFlip = useCallback(() => {
if (!disabled) setIsFlipped(!isFlipped)
}, [disabled, isFlipped])
const handleConfidenceSelect = useCallback((level: number) => {
if (disabled || hasAnswered) return
setSelectedConfidence(level)
onConfidenceSubmit(level)
}, [disabled, hasAnswered, onConfidenceSubmit])
return (
<div className="relative">
<div className="flex justify-end mb-2">
<ErrorReportButton onClick={onReportError} />
</div>
{/* 翻卡容器 */}
<div
className={`relative w-full ${disabled ? 'pointer-events-none opacity-75' : 'cursor-pointer'}`}
onClick={handleFlip}
style={{
perspective: '1000px',
height: `${cardHeight}px`,
minHeight: '400px',
transition: 'height 0.4s cubic-bezier(0.4, 0, 0.2, 1)'
}}
>
<div
style={{
position: 'relative',
width: '100%',
height: '100%',
transformStyle: 'preserve-3d',
transition: 'transform 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)'
}}
>
{/* 正面 */}
<div
ref={frontRef}
className="absolute w-full h-full bg-white rounded-xl shadow-lg hover:shadow-xl"
style={{ backfaceVisibility: 'hidden' }}
>
<div className="p-8 h-full">
<TestHeader
title="翻卡記憶"
difficultyLevel={cardData.cefr}
/>
<div className="space-y-4">
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
<div className="flex-1 flex items-center justify-center mt-6">
<div className="bg-gray-50 rounded-lg p-8 w-full text-center">
<h3 className="text-4xl font-bold text-gray-900 mb-6">{cardData.word}</h3>
<div className="flex items-center justify-center gap-3">
{cardData.pronunciation && (
<span className="text-lg text-gray-500">{cardData.pronunciation}</span>
)}
<div onClick={(e) => e.stopPropagation()}>
<AudioPlayer text={cardData.word} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 背面 */}
<div
ref={backRef}
className="absolute w-full h-full bg-white rounded-xl shadow-lg"
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)'
}}
>
<div className="p-8 h-full">
<TestHeader
title="翻卡記憶"
difficultyLevel={cardData.cefr}
/>
<div className="space-y-4 pb-6">
{/* 定義區塊 */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<p className="text-gray-700 text-left">{cardData.definition}</p>
</div>
{/* 例句區塊 */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="relative">
<p className="text-gray-700 italic mb-2 text-left pr-12">{cardData.example}</p>
<div className="absolute bottom-0 right-0" onClick={(e) => e.stopPropagation()}>
<AudioPlayer text={cardData.example} />
</div>
</div>
<p className="text-gray-600 text-sm text-left">{cardData.exampleTranslation}</p>
</div>
{/* 同義詞區塊 */}
{cardData.synonyms && cardData.synonyms.length > 0 && (
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left"></h3>
<div className="flex flex-wrap gap-2">
{cardData.synonyms.map((synonym, index) => (
<span
key={index}
className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200"
>
{synonym}
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* 信心等級評估區 - 使用新元件 */}
<div className="mt-6">
<ConfidenceLevel
selectedLevel={selectedConfidence}
onSelect={handleConfidenceSelect}
disabled={disabled || hasAnswered}
/>
</div>
</div>
)
}
export const FlipMemoryTest = memo(FlipMemoryTestComponent)
FlipMemoryTest.displayName = 'FlipMemoryTest'