255 lines
9.5 KiB
TypeScript
255 lines
9.5 KiB
TypeScript
'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>
|
||
)
|
||
} |