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:
parent
c01fd05450
commit
dba7666626
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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ˈpɔː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
|
||||
}
|
||||
Loading…
Reference in New Issue