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

266 lines
9.0 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 } from 'react'
import AudioPlayer from '@/components/AudioPlayer'
import { ErrorReportButton } from '@/components/review/shared'
interface FlipMemoryTestProps {
word: string
definition: string
example: string
exampleTranslation: string
pronunciation?: string
synonyms?: string[]
difficultyLevel: string
onConfidenceSubmit: (level: number) => void
onReportError: () => void
disabled?: boolean
}
export const FlipMemoryTest: React.FC<FlipMemoryTestProps> = ({
word,
definition,
example,
exampleTranslation,
pronunciation,
synonyms = [],
difficultyLevel,
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)
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)
}
}, [word, definition, example, synonyms])
const handleFlip = () => {
if (!disabled) setIsFlipped(!isFlipped)
}
const handleConfidenceSelect = (level: number) => {
if (disabled) return
setSelectedConfidence(level)
onConfidenceSubmit(level)
}
const confidenceLabels = {
1: '完全不懂',
2: '模糊',
3: '一般',
4: '熟悉',
5: '非常熟悉'
}
return (
<div className="relative">
<div className="flex justify-end mb-2">
<ErrorReportButton onClick={onReportError} />
</div>
{/* 翻卡容器 */}
<div
className={`card-container ${disabled ? 'pointer-events-none opacity-75' : 'cursor-pointer'}`}
onClick={handleFlip}
style={{ perspective: '1000px', height: `${cardHeight}px` }}
>
<div
className={`card bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-600 ${isFlipped ? 'rotate-y-180' : ''}`}
style={{ transformStyle: 'preserve-3d', height: '100%' }}
>
{/* 正面 */}
<div
ref={frontRef}
className="card-face card-front absolute w-full h-full"
style={{ backfaceVisibility: 'hidden' }}
>
<div className="p-8 h-full">
<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>
<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">{word}</h3>
<div className="flex items-center justify-center gap-3">
{pronunciation && (
<span className="text-lg text-gray-500">{pronunciation}</span>
)}
<div onClick={(e) => e.stopPropagation()}>
<AudioPlayer text={word} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 背面 */}
<div
ref={backRef}
className="card-face card-back absolute w-full h-full"
style={{ backfaceVisibility: 'hidden', transform: 'rotateY(180deg)' }}
>
<div className="p-8 h-full">
<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>
<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">{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">{example}</p>
<div className="absolute bottom-0 right-0" onClick={(e) => e.stopPropagation()}>
<AudioPlayer text={example} />
</div>
</div>
<p className="text-gray-600 text-sm text-left">{exampleTranslation}</p>
</div>
{/* 同義詞區塊 */}
{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">
{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">
<div className="grid grid-cols-5 gap-3">
{[1, 2, 3, 4, 5].map(level => (
<button
key={level}
onClick={() => handleConfidenceSelect(level)}
disabled={disabled || selectedConfidence !== null}
className={`py-4 px-3 border-2 rounded-lg transition-all text-center font-medium ${
selectedConfidence === level
? 'bg-blue-500 text-white border-blue-500 shadow-lg'
: 'bg-white border-gray-300 text-gray-700 hover:bg-blue-50 hover:border-blue-400 shadow-sm'
} ${disabled || selectedConfidence !== null ? 'opacity-50 cursor-not-allowed' : 'hover:shadow-md'}`}
>
<div className="text-lg font-semibold">
{confidenceLabels[level as keyof typeof confidenceLabels]}
</div>
</button>
))}
</div>
</div>
)}
<style jsx>{`
.card-container {
perspective: 1000px;
transition: height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
min-height: 400px;
}
.card {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.rotate-y-180 {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
overflow: hidden;
}
.card-front .p-8 {
display: flex;
flex-direction: column;
min-height: 100%;
}
.card-back .p-8 {
overflow: visible;
}
@media (max-width: 768px) {
.card-container {
min-height: 350px;
}
}
@media (max-width: 480px) {
.card-container {
min-height: 300px;
}
.card-face .p-8 {
padding: 1rem;
}
}
`}</style>
</div>
)
}