dramaling-vocab-learning/frontend/components/review/quiz/FlipMemory.tsx

255 lines
9.5 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.

'use client'
import { useState, useRef, useEffect, useCallback } from 'react'
import { CardState } from '@/lib/data/reviewSimpleData'
import { QuizHeader } from '../ui/QuizHeader'
import { BluePlayButton } from '@/components/shared/BluePlayButton'
interface SimpleFlipCardProps {
card: CardState
onAnswer: (confidence: number) => void
onSkip: () => void
}
export function FlipMemory({ card, onAnswer, onSkip }: SimpleFlipCardProps) {
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)
}
}, [card.word, card.definition, card.example, card.synonyms])
const handleFlip = useCallback(() => {
setIsFlipped(!isFlipped)
}, [isFlipped])
// 統一的卡片狀態重置函數
const resetCardState = useCallback(() => {
setIsFlipped(false)
setSelectedConfidence(null)
}, [])
const handleConfidenceSelect = useCallback((level: number) => {
if (hasAnswered) return
// 直接提交,不需要確認步驟
setSelectedConfidence(level)
onAnswer(level)
// 重置狀態為下一張卡片準備
setTimeout(resetCardState, 300) // 短暫延遲讓用戶看到選擇效果
}, [hasAnswered, onAnswer, resetCardState])
const handleSkipClick = useCallback(() => {
onSkip()
// 跳過後也重置卡片狀態
setTimeout(resetCardState, 100) // 較短延遲,因為沒有選擇效果需要顯示
}, [onSkip, resetCardState])
return (
<div className="relative">
{/* 翻卡容器 - 完全復用您的設計 */}
<div
className="relative w-full 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">
<QuizHeader
title="翻卡記憶"
cefr={card.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">{card.word}</h3>
<div className="flex items-center justify-center gap-3">
{card.pronunciation && (
<span className="text-lg text-gray-500">{card.pronunciation}</span>
)}
<div onClick={(e) => e.stopPropagation()}>
<BluePlayButton
text={card.word}
size="sm"
title="播放單詞發音"
rate={0.8}
lang="en-US"
/>
</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">
<QuizHeader
title="翻卡記憶"
cefr={card.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">{card.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">"{card.example}"</p>
<div className="absolute bottom-0 right-0" onClick={(e) => e.stopPropagation()}>
<BluePlayButton
text={card.example}
size="sm"
title="播放例句"
rate={0.7}
lang="en-US"
/>
</div>
</div>
<p className="text-gray-600 text-sm text-left">{card.exampleTranslation}</p>
</div>
{/* 同義詞區塊 */}
{card.synonyms && card.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">
{card.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="space-y-3">
<h3 className="text-lg font-semibold text-gray-900 mb-4 text-left">
</h3>
<div className="grid grid-cols-3 gap-3">
{[
{ level: 0, label: '模糊', color: 'bg-orange-100 text-orange-700 border-orange-200 hover:bg-orange-200' },
{ level: 1, label: '一般', color: 'bg-yellow-100 text-yellow-700 border-yellow-200 hover:bg-yellow-200' },
{ level: 2, label: '熟悉', color: 'bg-green-100 text-green-700 border-green-200 hover:bg-green-200' }
].map(({ level, label, color }) => {
const isSelected = selectedConfidence === level
return (
<button
key={level}
onClick={(e) => {
e.stopPropagation()
handleConfidenceSelect(level)
}}
className={`
px-3 py-2 rounded-lg border-2 text-center font-medium transition-all duration-200
${isSelected
? 'ring-2 ring-blue-400 ring-opacity-75 transform scale-105'
: 'cursor-pointer active:scale-95'
}
${color}
`}
>
<div className="flex items-center justify-center h-8">
<span className="text-sm">{label}</span>
</div>
</button>
)
})}
</div>
{/* 跳過按鈕 */}
<div className="mt-4">
<button
onClick={(e) => {
e.stopPropagation()
handleSkipClick()
}}
className="w-full border border-gray-300 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-50 transition-colors"
>
</button>
</div>
</div>
</div>
</div>
)
}