dramaling-vocab-learning/frontend/app/review-simple/components/SimpleFlipCard.tsx

228 lines
8.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 { ApiFlashcard } from '../data'
import { SimpleTestHeader } from './SimpleTestHeader'
interface SimpleFlipCardProps {
card: ApiFlashcard
onAnswer: (confidence: number) => void
}
export function SimpleFlipCard({ card, onAnswer }: 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 handleConfidenceSelect = useCallback((level: number) => {
if (hasAnswered) return
setSelectedConfidence(level)
}, [hasAnswered])
const handleSubmit = () => {
if (selectedConfidence) {
onAnswer(selectedConfidence)
// 重置狀態為下一張卡片準備
setIsFlipped(false)
setSelectedConfidence(null)
}
}
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">
<SimpleTestHeader
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>
</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">
<SimpleTestHeader
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>
<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-5 gap-3">
{[
{ level: 1, label: '完全不懂', color: 'bg-red-100 text-red-700 border-red-200 hover:bg-red-200' },
{ level: 2, label: '模糊', color: 'bg-orange-100 text-orange-700 border-orange-200 hover:bg-orange-200' },
{ level: 3, label: '一般', color: 'bg-yellow-100 text-yellow-700 border-yellow-200 hover:bg-yellow-200' },
{ level: 4, label: '熟悉', color: 'bg-blue-100 text-blue-700 border-blue-200 hover:bg-blue-200' },
{ level: 5, 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>
{/* 提交按鈕 - 選擇後顯示 */}
{hasAnswered && (
<button
onClick={(e) => {
e.stopPropagation()
handleSubmit()
}}
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-semibold hover:bg-blue-700 transition-colors mt-4"
>
</button>
)}
</div>
</div>
</div>
)
}