feat: 完全復用原始調教過的翻卡設計 + 真實API數據結構

## 核心升級
- 🎨 直接復用您調教過的 FlipMemoryTest 設計 (完美的高度計算)
- 📊 集成真實API數據結構 (api_seeds.json)
-  添加同義詞支持和顯示 (proof, testimony, documentation等)
- 🎯 保持極簡架構 + 專業設計的完美組合

## 設計完整性
-  智能響應式高度計算 (背面內容驅動)
-  完美的3D翻卡動畫 (cubic-bezier調校)
-  專業的內容區塊布局 (定義+例句+同義詞)
-  精美的信心度按鈕 (5色配置+動畫)

## 數據真實性
- 📚 真實學習詞彙: evidence, warrants, obtained, prioritize
- 📊 真實CEFR等級: B2, C1 專業難度
- 🎯 完整API響應格式 (為未來升級做準備)
-  智能同義詞映射 (增強學習價值)

現在擁有專業級的翻卡設計 + 真實學習內容 + 極簡可靠架構

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-10-04 03:30:25 +08:00
parent c01fd05450
commit dba7666626
3 changed files with 229 additions and 129 deletions

View File

@ -1,11 +1,11 @@
'use client'
import { useState, useRef, useEffect, useCallback } from 'react'
import { SimpleCard, CONFIDENCE_LEVELS } from '../data'
import { ApiFlashcard } from '../data'
import { SimpleTestHeader } from './SimpleTestHeader'
interface SimpleFlipCardProps {
card: SimpleCard
card: ApiFlashcard
onAnswer: (confidence: number) => void
}
@ -16,37 +16,43 @@ export function SimpleFlipCard({ card, onAnswer }: SimpleFlipCardProps) {
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)
window.addEventListener('resize', updateCardHeight)
return () => {
clearTimeout(timer)
window.removeEventListener('resize', updateCardHeight)
}
}, [card.word, card.definition, card.example])
}, [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) {
@ -57,13 +63,11 @@ export function SimpleFlipCard({ card, onAnswer }: SimpleFlipCardProps) {
}
}
const hasAnswered = selectedConfidence !== null
return (
<div className="w-full">
{/* 完全復用原FlipMemoryTest的精確設計 */}
<div className="relative">
{/* 翻卡容器 - 完全復用您的設計 */}
<div
className={`relative w-full cursor-pointer`}
className="relative w-full cursor-pointer"
onClick={handleFlip}
style={{
perspective: '1000px',
@ -82,7 +86,7 @@ export function SimpleFlipCard({ card, onAnswer }: SimpleFlipCardProps) {
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)'
}}
>
{/* 正面 - 完全復用原設計 */}
{/* 正面 */}
<div
ref={frontRef}
className="absolute w-full h-full bg-white rounded-xl shadow-lg hover:shadow-xl"
@ -91,10 +95,14 @@ export function SimpleFlipCard({ card, onAnswer }: SimpleFlipCardProps) {
<div className="p-8 h-full">
<SimpleTestHeader
title="翻卡記憶"
cefr="A1"
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>
@ -105,14 +113,11 @@ export function SimpleFlipCard({ card, onAnswer }: SimpleFlipCardProps) {
</div>
</div>
</div>
<p className="text-lg text-gray-700 mb-6 text-right">
</p>
</div>
</div>
</div>
{/* 背面 - 完全復用原設計 */}
{/* 背面 */}
<div
ref={backRef}
className="absolute w-full h-full bg-white rounded-xl shadow-lg"
@ -124,85 +129,100 @@ export function SimpleFlipCard({ card, onAnswer }: SimpleFlipCardProps) {
<div className="p-8 h-full">
<SimpleTestHeader
title="翻卡記憶"
cefr="A1"
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.translation}"</p>
<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>
{/* 信心等級評估區 - 完全復用原ConfidenceButtons設計 */}
{isFlipped && (
<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 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>
)
}

View File

@ -0,0 +1,78 @@
{
"success": true,
"data": {
"flashcards": [
{
"id": "1b3e2ec5-4729-4972-ae83-72782a624aa8",
"word": "evidence",
"translation": "證據",
"definition": "The available body of facts or information indicating whether a belief or proposition is true or valid.",
"partOfSpeech": "noun",
"pronunciation": "/ˈevɪdəns/",
"example": "There was no evidence of a crime.",
"exampleTranslation": "沒有犯罪的證據。",
"isFavorite": true,
"difficultyLevelNumeric": 4,
"cefr": "B2",
"createdAt": "2025-10-01T12:48:11.850357",
"updatedAt": "2025-10-01T13:37:22.91802",
"hasExampleImage": false,
"primaryImageUrl": null
},
{
"id": "5b854991-c64b-464f-b69b-f8946a165257",
"word": "warrants",
"translation": "搜索令",
"definition": "A document issued by a legal or government official authorizing the police or some other body to make an arrest, search premises, or carry out some other action relating to the administration of justice.",
"partOfSpeech": "noun",
"pronunciation": "/ˈwɒrənts/",
"example": "The judge issued the warrants.",
"exampleTranslation": "法官簽發了搜索令。",
"isFavorite": false,
"difficultyLevelNumeric": 5,
"cefr": "C1",
"createdAt": "2025-10-01T12:48:10.161318",
"updatedAt": "2025-10-01T12:48:10.161318",
"hasExampleImage": false,
"primaryImageUrl": null
},
{
"id": "d6f4227f-bdc9-4f13-a532-aa47f802cf8d",
"word": "obtained",
"translation": "獲得",
"definition": "To get or acquire something.",
"partOfSpeech": "verb",
"pronunciation": "/əbˈteɪnd/",
"example": "She obtained a degree in engineering.",
"exampleTranslation": "她獲得了工程學位。",
"isFavorite": false,
"difficultyLevelNumeric": 4,
"cefr": "B2",
"createdAt": "2025-10-01T12:48:07.640078",
"updatedAt": "2025-10-01T12:48:07.640111",
"hasExampleImage": true,
"primaryImageUrl": "/images/examples/d6f4227f-bdc9-4f13-a532-aa47f802cf8d_078eabb9-3630-4461-b9ea-98a677625d22.png"
},
{
"id": "26e2e99c-124f-4bfe-859e-8819c68e72b8",
"word": "prioritize",
"translation": "優先安排",
"definition": "designate or treat (something) as being more important than other things",
"partOfSpeech": "verb",
"pronunciation": "/praɪˈɒrɪtaɪz/",
"example": "We need to prioritize our tasks.",
"exampleTranslation": "我們需要優先安排我們的任務。",
"isFavorite": true,
"difficultyLevelNumeric": 4,
"cefr": "B2",
"createdAt": "2025-09-30T18:02:36.316465",
"updatedAt": "2025-10-01T15:49:08.525139",
"hasExampleImage": true,
"primaryImageUrl": "/images/examples/26e2e99c-124f-4bfe-859e-8819c68e72b8_a7923c26-fefd-4705-9921-dc81f44e47c0.png"
}
],
"count": 4
},
"message": null,
"timestamp": "2025-10-03T18:57:25.802194Z"
}

View File

@ -1,61 +1,63 @@
// 極簡MVP的靜態測試數據
export interface SimpleCard {
id: number
// 模擬真實API數據結構
import apiSeeds from './components/api_seeds.json'
// API響應接口 (匹配真實API結構 + 同義詞擴展)
export interface ApiFlashcard {
id: string
word: string
definition: string
example: string
translation: string
definition: string
partOfSpeech: string
pronunciation: string
example: string
exampleTranslation: string
isFavorite: boolean
difficultyLevelNumeric: number
cefr: string
createdAt: string
updatedAt: string
hasExampleImage: boolean
primaryImageUrl: string | null
// 添加同義詞支持
synonyms?: string[]
}
export const SIMPLE_CARDS: SimpleCard[] = [
{
id: 1,
word: 'hello',
definition: 'a greeting or expression of welcome',
example: 'Hello, how are you today?',
translation: '你好',
pronunciation: '/həˈloʊ/'
},
{
id: 2,
word: 'beautiful',
definition: 'pleasing the senses or mind aesthetically',
example: 'She has a beautiful smile.',
translation: '美麗的',
pronunciation: '/ˈbjuːtɪfl/'
},
{
id: 3,
word: 'important',
definition: 'of great significance or value',
example: 'It is important to study every day.',
translation: '重要的',
pronunciation: '/ɪmˈːrtənt/'
},
{
id: 4,
word: 'happy',
definition: 'feeling or showing pleasure or contentment',
example: 'I am very happy today.',
translation: '快樂的',
pronunciation: '/ˈhæpi/'
},
{
id: 5,
word: 'learn',
definition: 'gain knowledge or skill by studying or experience',
example: 'I want to learn English.',
translation: '學習',
pronunciation: '/lɜːrn/'
export interface ApiResponse {
success: boolean
data: {
flashcards: ApiFlashcard[]
count: number
}
]
message: string | null
timestamp: string
}
// 信心度等級配置
export const CONFIDENCE_LEVELS = [
{ level: 1, label: '完全不懂', color: 'bg-red-500', description: '第一次見到這個詞' },
{ level: 2, label: '有點印象', color: 'bg-orange-500', description: '似乎見過但不確定意思' },
{ level: 3, label: '還算熟悉', color: 'bg-yellow-500', description: '知道意思但不太確定' },
{ level: 4, label: '很熟悉', color: 'bg-blue-500', description: '清楚知道意思和用法' },
{ level: 5, label: '完全掌握', color: 'bg-green-500', description: '可以自然使用' }
] as const
// 模擬API響應數據 (直接使用真實API格式)
export const MOCK_API_RESPONSE: ApiResponse = apiSeeds as ApiResponse
// 為API數據添加同義詞 (模擬完整數據)
const addSynonyms = (flashcards: any[]): ApiFlashcard[] => {
const synonymsMap: Record<string, string[]> = {
'evidence': ['proof', 'testimony', 'documentation'],
'warrants': ['authorizations', 'permits', 'orders'],
'obtained': ['acquired', 'gained', 'secured'],
'prioritize': ['rank', 'organize', 'arrange']
}
return flashcards.map(card => ({
...card,
synonyms: synonymsMap[card.word] || []
}))
}
// 提取詞卡數據 (方便組件使用)
export const SIMPLE_CARDS = addSynonyms(MOCK_API_RESPONSE.data.flashcards)
// 模擬API調用函數 (為未來API集成做準備)
export const mockApiCall = async (): Promise<ApiResponse> => {
// 模擬網路延遲
await new Promise(resolve => setTimeout(resolve, 500))
// 返回模擬數據
return MOCK_API_RESPONSE
}