dramaling-vocab-learning/frontend/app/flashcards/[id]/page.tsx

774 lines
32 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, useEffect, use } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { Navigation } from '@/components/Navigation'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { useToast } from '@/components/Toast'
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
import { imageGenerationService } from '@/lib/services/imageGeneration'
interface FlashcardDetailPageProps {
params: Promise<{
id: string
}>
}
export default function FlashcardDetailPage({ params }: FlashcardDetailPageProps) {
const { id } = use(params)
return (
<ProtectedRoute>
<FlashcardDetailContent cardId={id} />
</ProtectedRoute>
)
}
function FlashcardDetailContent({ cardId }: { cardId: string }) {
const router = useRouter()
const searchParams = useSearchParams()
const toast = useToast()
const [flashcard, setFlashcard] = useState<Flashcard | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isEditing, setIsEditing] = useState(false)
const [editedCard, setEditedCard] = useState<any>(null)
// 圖片生成狀態
const [isGeneratingImage, setIsGeneratingImage] = useState(false)
const [generationProgress, setGenerationProgress] = useState<string>('')
const [isPlayingWord, setIsPlayingWord] = useState(false)
const [isPlayingExample, setIsPlayingExample] = useState(false)
// TTS播放控制 - 詞彙發音
const toggleWordTTS = (text: string, lang: string = 'en-US') => {
if (!('speechSynthesis' in window)) {
toast.error('您的瀏覽器不支援語音播放');
return;
}
// 如果正在播放詞彙,則停止
if (isPlayingWord) {
speechSynthesis.cancel();
setIsPlayingWord(false);
return;
}
// 停止所有播放並開始新播放
speechSynthesis.cancel();
setIsPlayingWord(true);
setIsPlayingExample(false);
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = lang;
utterance.rate = 0.8; // 詞彙播放稍慢
utterance.pitch = 1.0;
utterance.volume = 1.0;
utterance.onend = () => setIsPlayingWord(false);
utterance.onerror = () => {
setIsPlayingWord(false);
toast.error('語音播放失敗');
};
speechSynthesis.speak(utterance);
}
// TTS播放控制 - 例句發音
const toggleExampleTTS = (text: string, lang: string = 'en-US') => {
if (!('speechSynthesis' in window)) {
toast.error('您的瀏覽器不支援語音播放');
return;
}
// 如果正在播放例句,則停止
if (isPlayingExample) {
speechSynthesis.cancel();
setIsPlayingExample(false);
return;
}
// 停止所有播放並開始新播放
speechSynthesis.cancel();
setIsPlayingExample(true);
setIsPlayingWord(false);
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = lang;
utterance.rate = 0.9; // 例句播放正常語速
utterance.pitch = 1.0;
utterance.volume = 1.0;
utterance.onend = () => setIsPlayingExample(false);
utterance.onerror = () => {
setIsPlayingExample(false);
toast.error('語音播放失敗');
};
speechSynthesis.speak(utterance);
}
// 假資料 - 用於展示效果
const mockCards: {[key: string]: any} = {
'mock1': {
id: 'mock1',
word: 'hello',
translation: '你好',
partOfSpeech: 'interjection',
pronunciation: '/həˈloʊ/',
definition: 'A greeting word used when meeting someone or beginning a phone conversation',
example: 'Hello, how are you today?',
exampleTranslation: '你好,你今天怎麼樣?',
masteryLevel: 95,
timesReviewed: 15,
isFavorite: true,
nextReviewDate: '2025-09-21',
cardSet: { name: '基礎詞彙', color: 'bg-blue-500' },
cefr: 'A1',
createdAt: '2025-09-17',
synonyms: ['hi', 'greetings', 'good day'],
// 添加圖片欄位
exampleImages: [],
hasExampleImage: false,
primaryImageUrl: null
},
'mock2': {
id: 'mock2',
word: 'elaborate',
translation: '詳細說明',
partOfSpeech: 'verb',
pronunciation: '/ɪˈlæbərət/',
definition: 'To explain something in more detail; to develop or present a theory, policy, or system in further detail',
example: 'Could you elaborate on your proposal?',
exampleTranslation: '你能詳細說明一下你的提案嗎?',
masteryLevel: 45,
timesReviewed: 5,
isFavorite: false,
nextReviewDate: '2025-09-19',
cardSet: { name: '高級詞彙', color: 'bg-purple-500' },
cefr: 'B2',
createdAt: '2025-09-14',
synonyms: ['explain', 'detail', 'expand', 'clarify'],
// 添加圖片欄位
exampleImages: [],
hasExampleImage: false,
primaryImageUrl: null
}
}
// 載入詞卡資料
useEffect(() => {
const loadFlashcard = async () => {
try {
setLoading(true)
// 首先檢查是否為假資料
if (mockCards[cardId]) {
setFlashcard(mockCards[cardId])
setEditedCard(mockCards[cardId])
setLoading(false)
return
}
// 載入真實詞卡 - 使用直接 API 調用
const result = await flashcardsService.getFlashcard(cardId)
if (result.success && result.data) {
setFlashcard(result.data)
setEditedCard(result.data)
} else {
throw new Error(result.error || '詞卡不存在')
}
} catch (err) {
setError('載入詞卡時發生錯誤')
} finally {
setLoading(false)
}
}
loadFlashcard()
}, [cardId])
// 檢查 URL 參數,自動開啟編輯模式
useEffect(() => {
if (searchParams.get('edit') === 'true' && flashcard) {
setIsEditing(true)
// 清理 URL 參數,保持 URL 乾淨
router.replace(`/flashcards/${cardId}`)
}
}, [flashcard, searchParams, cardId, router])
// 獲取CEFR等級顏色
const getCEFRColor = (level: string) => {
switch (level) {
case 'A1': return 'bg-green-100 text-green-700 border-green-200'
case 'A2': return 'bg-blue-100 text-blue-700 border-blue-200'
case 'B1': return 'bg-yellow-100 text-yellow-700 border-yellow-200'
case 'B2': return 'bg-orange-100 text-orange-700 border-orange-200'
case 'C1': return 'bg-red-100 text-red-700 border-red-200'
case 'C2': return 'bg-purple-100 text-purple-700 border-purple-200'
default: return 'bg-gray-100 text-gray-700 border-gray-200'
}
}
// 獲取例句圖片 - 使用 API 資料
const getExampleImage = (card: Flashcard): string | null => {
return card.primaryImageUrl || null
}
// 詞性簡寫轉換
const getPartOfSpeechDisplay = (partOfSpeech: string): string => {
const shortMap: {[key: string]: string} = {
'noun': 'n.',
'verb': 'v.',
'adjective': 'adj.',
'adverb': 'adv.',
'pronoun': 'pron.',
'conjunction': 'conj.',
'preposition': 'prep.',
'interjection': 'int.',
'idiom': 'idiom'
}
// 處理複合詞性 (如 "preposition/adverb")
if (partOfSpeech?.includes('/')) {
return partOfSpeech.split('/').map(p => shortMap[p.trim()] || p.trim()).join('/')
}
return shortMap[partOfSpeech] || partOfSpeech || ''
}
// 處理收藏切換
const handleToggleFavorite = async () => {
if (!flashcard) return
try {
// 假資料處理
if (flashcard.id.startsWith('mock')) {
const updated = { ...flashcard, isFavorite: !flashcard.isFavorite }
setFlashcard(updated)
setEditedCard(updated)
toast.success(`${flashcard.isFavorite ? '已取消收藏' : '已加入收藏'}${flashcard.word}`)
return
}
// 真實API調用
const result = await flashcardsService.toggleFavorite(flashcard.id)
if (result.success) {
setFlashcard(prev => prev ? { ...prev, isFavorite: !prev.isFavorite } : null)
toast.success(`${flashcard.isFavorite ? '已取消收藏' : '已加入收藏'}${flashcard.word}`)
}
} catch (error) {
toast.error('操作失敗,請重試')
}
}
// 處理編輯保存
const handleSaveEdit = async () => {
if (!flashcard || !editedCard) return
try {
// 假資料處理
if (flashcard.id.startsWith('mock')) {
setFlashcard(editedCard)
setIsEditing(false)
toast.success('詞卡更新成功!')
return
}
// 真實API調用
const result = await flashcardsService.updateFlashcard(flashcard.id, {
word: editedCard.word,
translation: editedCard.translation,
definition: editedCard.definition,
pronunciation: editedCard.pronunciation,
partOfSpeech: editedCard.partOfSpeech,
example: editedCard.example,
exampleTranslation: editedCard.exampleTranslation,
cefr: editedCard.cefr
})
if (result.success) {
setFlashcard(editedCard)
setIsEditing(false)
toast.success('詞卡更新成功!')
} else {
toast.error(result.error || '更新失敗')
}
} catch (error) {
toast.error('更新失敗,請重試')
}
}
// 處理刪除
const handleDelete = async () => {
if (!flashcard) return
if (!confirm(`確定要刪除詞卡「${flashcard.word}」嗎?`)) {
return
}
try {
// 假資料處理
if (flashcard.id.startsWith('mock')) {
toast.success('詞卡已刪除(模擬)')
router.push('/flashcards')
return
}
// 真實API調用
const result = await flashcardsService.deleteFlashcard(flashcard.id)
if (result.success) {
toast.success('詞卡已刪除')
router.push('/flashcards')
} else {
toast.error(result.error || '刪除失敗')
}
} catch (error) {
toast.error('刪除失敗,請重試')
}
}
// 處理圖片生成
const handleGenerateImage = async () => {
if (!flashcard || isGeneratingImage) return
try {
setIsGeneratingImage(true)
setGenerationProgress('啟動生成中...')
toast.info(`開始為「${flashcard.word}」生成例句圖片...`)
const generateResult = await imageGenerationService.generateImage(flashcard.id)
if (!generateResult.success || !generateResult.data) {
throw new Error(generateResult.error || '啟動生成失敗')
}
const requestId = generateResult.data.requestId
setGenerationProgress('Gemini 生成描述中...')
const finalStatus = await imageGenerationService.pollUntilComplete(
requestId,
(status: any) => {
const stage = status.stages.gemini.status === 'completed'
? 'Replicate 生成圖片中...' : 'Gemini 生成描述中...'
setGenerationProgress(stage)
},
5
)
if (finalStatus.overallStatus === 'completed') {
setGenerationProgress('生成完成,載入中...')
// 重新載入詞卡資料
const result = await flashcardsService.getFlashcard(cardId)
if (result.success && result.data) {
setFlashcard(result.data)
setEditedCard(result.data)
}
toast.success(`${flashcard.word}」的例句圖片生成完成!`)
} else {
throw new Error('圖片生成未完成')
}
} catch (error: any) {
toast.error(`圖片生成失敗: ${error.message || '未知錯誤'}`)
} finally {
setIsGeneratingImage(false)
setGenerationProgress('')
}
}
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-lg text-gray-600">...</div>
</div>
)
}
if (error || !flashcard) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="text-red-600 text-lg mb-4">{error || '詞卡不存在'}</div>
<button
onClick={() => router.push('/flashcards')}
className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors"
>
</button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<Navigation />
<div className="max-w-4xl mx-auto px-4 py-8">
{/* 導航欄 */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<button
onClick={() => router.push('/flashcards')}
className="text-gray-600 hover:text-gray-900 flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
</div>
</div>
{/* 主要詞卡內容 - 學習功能風格 */}
<div className="bg-white rounded-xl shadow-lg overflow-hidden mb-6 relative">
{/* CEFR標籤 - 右上角 */}
<div className="absolute top-4 right-4 z-10">
<span className={`px-3 py-1 rounded-full text-sm font-medium border ${getCEFRColor(flashcard.cefr || 'A1')}`}>
{flashcard.cefr || 'A1'}
</span>
</div>
{/* 標題區 */}
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 border-b border-blue-200">
<div className="pr-16">
<h1 className="text-4xl font-bold text-gray-900 mb-3">{flashcard.word}</h1>
<div className="flex items-center gap-3">
<span className="text-sm bg-gray-100 text-gray-700 px-3 py-1 rounded-full">
{getPartOfSpeechDisplay(flashcard.partOfSpeech)}
</span>
<span className="text-lg text-gray-600">{flashcard.pronunciation}</span>
<button
onClick={() => toggleWordTTS(flashcard.word, 'en-US')}
disabled={isPlayingExample}
title={isPlayingWord ? "點擊停止播放" : "點擊聽詞彙發音"}
aria-label={isPlayingWord ? `停止播放詞彙:${flashcard.word}` : `播放詞彙發音:${flashcard.word}`}
className={`group relative w-12 h-12 rounded-full shadow-lg transform transition-all duration-200
${isPlayingWord
? 'bg-gradient-to-br from-green-500 to-green-600 shadow-green-200 scale-105'
: 'bg-gradient-to-br from-blue-500 to-blue-600 hover:shadow-xl hover:scale-105 shadow-blue-200'
} ${isPlayingExample ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}
`}
>
{/* 播放中波紋效果 */}
{isPlayingWord && (
<div className="absolute inset-0 bg-blue-400 rounded-full animate-ping opacity-40"></div>
)}
{/* 按鈕圖標 */}
<div className="relative z-10 flex items-center justify-center w-full h-full">
{isPlayingWord ? (
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
</svg>
) : (
<svg className="w-6 h-6 text-white group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
)}
</div>
{/* 懸停提示光環 */}
{!isPlayingWord && !isPlayingExample && (
<div className="absolute inset-0 rounded-full bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-200"></div>
)}
</button>
</div>
</div>
{/* 學習統計 */}
<div className="grid grid-cols-3 gap-4 text-center mt-4">
<div className="bg-white bg-opacity-60 rounded-lg p-3">
<div className="text-2xl font-bold text-gray-900">{flashcard.masteryLevel}%</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white bg-opacity-60 rounded-lg p-3">
<div className="text-2xl font-bold text-gray-900">{flashcard.timesReviewed}</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white bg-opacity-60 rounded-lg p-3">
<div className="text-2xl font-bold text-gray-900">
{Math.ceil((new Date(flashcard.nextReviewDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))}
</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
</div>
{/* 內容區 - 學習卡片風格 */}
<div className="p-6 space-y-6">
{/* 翻譯區塊 */}
<div className="bg-green-50 rounded-lg p-4 border border-green-200">
<h3 className="font-semibold text-green-900 mb-3 text-left"></h3>
{isEditing ? (
<input
type="text"
value={editedCard?.translation || ''}
onChange={(e) => setEditedCard((prev: any) => ({ ...prev, translation: e.target.value }))}
className="w-full p-3 border border-green-300 rounded-lg focus:ring-2 focus:ring-green-500 bg-white"
placeholder="輸入中文翻譯"
/>
) : (
<p className="text-green-800 font-medium text-left text-lg">
{flashcard.translation}
</p>
)}
</div>
{/* 定義區塊 */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h3 className="font-semibold text-gray-900 mb-3 text-left"></h3>
{isEditing ? (
<textarea
value={editedCard?.definition || ''}
onChange={(e) => setEditedCard((prev: any) => ({ ...prev, definition: e.target.value }))}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-20 resize-none"
placeholder="輸入英文定義"
/>
) : (
<p className="text-gray-700 text-left leading-relaxed">
{flashcard.definition}
</p>
)}
</div>
{/* 例句區塊 */}
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
<h3 className="font-semibold text-blue-900 mb-3 text-left"></h3>
{/* 例句圖片 */}
<div className="mb-4 relative">
{getExampleImage(flashcard) ? (
<img
src={getExampleImage(flashcard)!}
alt={`${flashcard.word} example`}
className="w-full max-w-md mx-auto rounded-lg border border-blue-300"
/>
) : (
<div className="w-full max-w-md mx-auto h-48 bg-gray-100 rounded-lg border-2 border-dashed border-gray-300 flex items-center justify-center">
<div className="text-center text-gray-500">
<svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-sm"></p>
<button
onClick={handleGenerateImage}
disabled={isGeneratingImage}
className="mt-2 px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors disabled:opacity-50"
>
{isGeneratingImage ? generationProgress : '生成圖片'}
</button>
</div>
</div>
)}
{/* 圖片上的生成按鈕 */}
{getExampleImage(flashcard) && !isGeneratingImage && (
<button
onClick={handleGenerateImage}
className="absolute top-2 right-2 px-2 py-1 text-xs bg-white bg-opacity-90 text-gray-700 rounded-md hover:bg-opacity-100 transition-all shadow-sm"
>
</button>
)}
{/* 生成進度覆蓋 */}
{isGeneratingImage && getExampleImage(flashcard) && (
<div className="absolute inset-0 bg-white bg-opacity-90 rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
<p className="text-sm text-gray-600">{generationProgress}</p>
</div>
</div>
)}
</div>
<div className="space-y-3">
{isEditing ? (
<>
<textarea
value={editedCard?.example || ''}
onChange={(e) => setEditedCard((prev: any) => ({ ...prev, example: e.target.value }))}
className="w-full p-3 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-16 resize-none"
placeholder="輸入英文例句"
/>
<textarea
value={editedCard?.exampleTranslation || ''}
onChange={(e) => setEditedCard((prev: any) => ({ ...prev, exampleTranslation: e.target.value }))}
className="w-full p-3 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-16 resize-none"
placeholder="輸入例句翻譯"
/>
</>
) : (
<>
<div className="relative">
<p className="text-blue-800 text-left italic text-lg pr-12">
"{flashcard.example}"
</p>
<div className="absolute bottom-0 right-0">
<button
onClick={() => toggleExampleTTS(flashcard.example, 'en-US')}
disabled={isPlayingWord}
title={isPlayingExample ? "點擊停止播放" : "點擊聽例句發音"}
aria-label={isPlayingExample ? `停止播放例句:${flashcard.example}` : `播放例句發音:${flashcard.example}`}
className={`group relative w-12 h-12 rounded-full shadow-lg transform transition-all duration-200
${isPlayingExample
? 'bg-gradient-to-br from-green-500 to-green-600 shadow-green-200 scale-105'
: 'bg-gradient-to-br from-blue-500 to-blue-600 hover:shadow-xl hover:scale-105 shadow-blue-200'
} ${isPlayingWord ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}
`}
>
{/* 播放中波紋效果 */}
{isPlayingExample && (
<div className="absolute inset-0 bg-blue-400 rounded-full animate-ping opacity-40"></div>
)}
{/* 按鈕圖標 */}
<div className="relative z-10 flex items-center justify-center w-full h-full">
{isPlayingExample ? (
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
</svg>
) : (
<svg className="w-6 h-6 text-white group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
)}
</div>
{/* 懸停提示光環 */}
{!isPlayingWord && !isPlayingExample && (
<div className="absolute inset-0 rounded-full bg-white opacity-0 group-hover:opacity-20 transition-opacity duration-200"></div>
)}
</button>
</div>
</div>
<p className="text-blue-700 text-left text-base">
"{flashcard.exampleTranslation}"
</p>
</>
)}
</div>
</div>
{/* 同義詞區塊 */}
{(flashcard as any).synonyms && (flashcard as any).synonyms.length > 0 && (
<div className="bg-purple-50 rounded-lg p-4 border border-purple-200">
<h3 className="font-semibold text-purple-900 mb-3 text-left"></h3>
<div className="flex flex-wrap gap-2">
{(flashcard as any).synonyms.map((synonym: string, index: number) => (
<span
key={index}
className="bg-white text-purple-700 px-3 py-1 rounded-full text-sm border border-purple-200 font-medium"
>
{synonym}
</span>
))}
</div>
</div>
)}
{/* 詞卡資訊 */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h3 className="font-semibold text-gray-900 mb-3 text-left"></h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">:</span>
<span className="ml-2 font-medium">{getPartOfSpeechDisplay(flashcard.partOfSpeech)}</span>
</div>
<div>
<span className="text-gray-600">:</span>
<span className="ml-2 font-medium">{new Date(flashcard.createdAt).toLocaleDateString()}</span>
</div>
<div>
<span className="text-gray-600">:</span>
<span className="ml-2 font-medium">{new Date(flashcard.nextReviewDate).toLocaleDateString()}</span>
</div>
<div>
<span className="text-gray-600">:</span>
<span className="ml-2 font-medium">{flashcard.timesReviewed} </span>
</div>
</div>
</div>
</div>
{/* 編輯模式的操作按鈕 */}
{isEditing && (
<div className="px-6 pb-6">
<div className="flex gap-3">
<button
onClick={handleSaveEdit}
className="flex-1 bg-green-600 text-white py-3 rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</button>
<button
onClick={() => {
setIsEditing(false)
setEditedCard(flashcard)
}}
className="flex-1 bg-gray-500 text-white py-3 rounded-lg font-medium hover:bg-gray-600 transition-colors"
>
</button>
</div>
</div>
)}
</div>
{/* 底部操作區 - 平均延展按鈕 */}
<div className="flex gap-3">
<button
onClick={handleToggleFavorite}
className={`flex-1 py-3 rounded-lg font-medium transition-colors ${
flashcard.isFavorite
? 'bg-yellow-100 text-yellow-700 border border-yellow-300 hover:bg-yellow-200'
: 'bg-gray-100 text-gray-600 border border-gray-300 hover:bg-yellow-50 hover:text-yellow-600'
}`}
>
<div className="flex items-center justify-center gap-2">
<svg className="w-4 h-4" fill={flashcard.isFavorite ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
{flashcard.isFavorite ? '已收藏' : '收藏'}
</div>
</button>
<button
onClick={() => setIsEditing(!isEditing)}
className={`flex-1 py-3 rounded-lg font-medium transition-colors ${
isEditing
? 'bg-gray-100 text-gray-700 border border-gray-300'
: 'bg-blue-100 text-blue-700 border border-blue-300 hover:bg-blue-200'
}`}
>
<div className="flex items-center justify-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
{isEditing ? '取消編輯' : '編輯詞卡'}
</div>
</button>
<button
onClick={handleDelete}
className="flex-1 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors"
>
<div className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
</button>
</div>
{/* Toast 通知系統 */}
<toast.ToastContainer />
</div>
</div>
)
}