dramaling-vocab-learning/frontend/app/flashcards/page.tsx

707 lines
34 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 } from 'react'
import Link from 'next/link'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { Navigation } from '@/components/Navigation'
import { FlashcardForm } from '@/components/FlashcardForm'
import { flashcardsService, type CardSet, type Flashcard } from '@/lib/services/flashcards'
function FlashcardsContent() {
const [activeTab, setActiveTab] = useState('my-cards')
const [selectedSet, setSelectedSet] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
// Real data from API
const [cardSets, setCardSets] = useState<CardSet[]>([])
const [flashcards, setFlashcards] = useState<Flashcard[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// 臨時使用學習功能的例句圖片作為測試
const getExampleImage = (word: string): string => {
const imageMap: {[key: string]: string} = {
'brought': '/images/examples/bring_up.png',
'instincts': '/images/examples/instinct.png',
'warrants': '/images/examples/warrant.png',
'elaborate': '/images/examples/bring_up.png', // 作為預設
'hello': '/images/examples/instinct.png',
'good': '/images/examples/warrant.png'
}
// 根據詞彙返回對應圖片,如果沒有則返回隨機圖片
return imageMap[word?.toLowerCase()] ||
imageMap[Object.keys(imageMap)[Math.floor(Math.random() * Object.keys(imageMap).length)]]
}
// Form states
const [showForm, setShowForm] = useState(false)
const [editingCard, setEditingCard] = useState<Flashcard | null>(null)
// 添加假資料用於展示CEFR效果
const mockFlashcards = [
{ id: 'mock1', word: 'hello', translation: '你好', partOfSpeech: 'interjection', pronunciation: '/həˈloʊ/', masteryLevel: 95, timesReviewed: 15, isFavorite: true, nextReviewDate: '2025-09-21', cardSet: { name: '基礎詞彙', color: 'bg-blue-500' }, difficultyLevel: 'A1' },
{ id: 'mock2', word: 'beautiful', translation: '美麗的', partOfSpeech: 'adjective', pronunciation: '/ˈbjuːtɪfəl/', masteryLevel: 78, timesReviewed: 8, isFavorite: false, nextReviewDate: '2025-09-22', cardSet: { name: '描述詞彙', color: 'bg-green-500' }, difficultyLevel: 'A2' },
{ id: 'mock3', word: 'understand', translation: '理解', partOfSpeech: 'verb', pronunciation: '/ˌʌndərˈstænd/', masteryLevel: 65, timesReviewed: 12, isFavorite: true, nextReviewDate: '2025-09-20', cardSet: { name: '常用動詞', color: 'bg-yellow-500' }, difficultyLevel: 'B1' },
{ id: 'mock4', word: 'elaborate', translation: '詳細說明', partOfSpeech: 'verb', pronunciation: '/ɪˈlæbərət/', masteryLevel: 45, timesReviewed: 5, isFavorite: false, nextReviewDate: '2025-09-19', cardSet: { name: '高級詞彙', color: 'bg-purple-500' }, difficultyLevel: 'B2' },
{ id: 'mock5', word: 'sophisticated', translation: '精密的', partOfSpeech: 'adjective', pronunciation: '/səˈfɪstɪkeɪtɪd/', masteryLevel: 30, timesReviewed: 3, isFavorite: true, nextReviewDate: '2025-09-18', cardSet: { name: '進階詞彙', color: 'bg-indigo-500' }, difficultyLevel: 'C1' },
{ id: 'mock6', word: 'ubiquitous', translation: '無處不在的', partOfSpeech: 'adjective', pronunciation: '/juːˈbɪkwɪtəs/', masteryLevel: 15, timesReviewed: 1, isFavorite: false, nextReviewDate: '2025-09-17', cardSet: { name: '學術詞彙', color: 'bg-red-500' }, difficultyLevel: 'C2' }
]
// Load data from API
useEffect(() => {
loadCardSets()
loadFlashcards()
}, [])
const loadCardSets = async () => {
try {
const result = await flashcardsService.getCardSets()
if (result.success && result.data) {
if (result.data.sets.length === 0) {
// 如果沒有卡組,確保創建預設卡組
const ensureResult = await flashcardsService.ensureDefaultCardSet()
if (ensureResult.success) {
// 重新載入卡組
const retryResult = await flashcardsService.getCardSets()
if (retryResult.success && retryResult.data) {
setCardSets(retryResult.data.sets)
} else {
setError('Failed to load card sets after creating default')
}
} else {
setError(ensureResult.error || 'Failed to create default card set')
}
} else {
setCardSets(result.data.sets)
}
} else {
setError(result.error || 'Failed to load card sets')
}
} catch (err) {
setError('Failed to load card sets')
}
}
const loadFlashcards = async () => {
try {
setLoading(true)
const result = await flashcardsService.getFlashcards(selectedSet || undefined)
if (result.success && result.data) {
setFlashcards(result.data.flashcards)
} else {
setError(result.error || 'Failed to load flashcards')
}
} catch (err) {
setError('Failed to load flashcards')
} finally {
setLoading(false)
}
}
// Reload flashcards when selectedSet changes
useEffect(() => {
loadFlashcards()
}, [selectedSet])
// Handle form operations
const handleFormSuccess = () => {
setShowForm(false)
setEditingCard(null)
loadFlashcards()
loadCardSets()
}
const handleEdit = (card: Flashcard) => {
setEditingCard(card)
setShowForm(true)
}
const handleDelete = async (card: Flashcard) => {
if (!confirm(`確定要刪除詞卡「${card.word}」嗎?`)) {
return
}
try {
const result = await flashcardsService.deleteFlashcard(card.id)
if (result.success) {
loadFlashcards()
loadCardSets()
} else {
alert(result.error || '刪除失敗')
}
} catch (err) {
alert('刪除失敗,請重試')
}
}
const handleToggleFavorite = async (card: any) => {
try {
// 如果是假資料,只更新本地狀態
if (card.id.startsWith('mock')) {
const updatedMockCards = mockFlashcards.map(mockCard =>
mockCard.id === card.id
? { ...mockCard, isFavorite: !mockCard.isFavorite }
: mockCard
)
// 這裡需要更新state但由於是const我們直接重新載入頁面來模擬效果
alert(`${card.isFavorite ? '已取消收藏' : '已加入收藏'}${card.word}`)
return
}
// 真實API調用
const result = await flashcardsService.toggleFavorite(card.id)
if (result.success) {
loadFlashcards()
alert(`${card.isFavorite ? '已取消收藏' : '已加入收藏'}${card.word}`)
} else {
alert(result.error || '操作失敗')
}
} catch (err) {
alert('操作失敗,請重試')
}
}
// 獲取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' // 預設灰色
}
}
// Filter data - 合併真實資料和假資料
const filteredSets = cardSets.filter(set =>
set.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
set.description.toLowerCase().includes(searchTerm.toLowerCase())
)
const allCards = [...flashcards, ...mockFlashcards] // 合併真實和假資料
const filteredCards = allCards.filter(card => {
if (searchTerm) {
return card.word?.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.translation?.toLowerCase().includes(searchTerm.toLowerCase())
}
return true
})
// Add loading and error states
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-lg">...</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-red-600">{error}</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<Navigation />
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Page Header */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="mt-1 text-sm text-gray-500"></p>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => setShowForm(true)}
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors"
>
</button>
<Link
href="/generate"
className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary/90 transition-colors"
>
AI
</Link>
</div>
</div>
{/* Tabs */}
<div className="flex space-x-8 mb-6 border-b border-gray-200">
<button
onClick={() => setActiveTab('my-cards')}
className={`pb-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'my-cards'
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
({filteredSets.length})
</button>
<button
onClick={() => setActiveTab('all-cards')}
className={`pb-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'all-cards'
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
({filteredCards.length})
</button>
<button
onClick={() => setActiveTab('favorites')}
className={`pb-4 px-1 border-b-2 font-medium text-sm flex items-center gap-1 ${
activeTab === 'favorites'
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<span className="text-yellow-500"></span>
({allCards.filter(card => card.isFavorite).length})
</button>
<button
onClick={() => {
const defaultSet = cardSets.find(set => set.isDefault)
if (defaultSet) {
setSelectedSet(defaultSet.id)
setActiveTab('all-cards')
}
}}
className={`pb-4 px-1 border-b-2 font-medium text-sm ${
selectedSet && cardSets.find(set => set.id === selectedSet)?.isDefault
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
📂
</button>
</div>
{/* Search */}
<div className="mb-6">
<div className="relative">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜尋詞卡或卡組..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
{/* Card Sets Tab */}
{activeTab === 'my-cards' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold"> {filteredSets.length} </h3>
</div>
{filteredSets.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 mb-4"></p>
<Link
href="/generate"
className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary/90 transition-colors"
>
</Link>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredSets.map(set => (
<div
key={set.id}
className={`border rounded-lg hover:shadow-lg transition-shadow cursor-pointer ${
set.isDefault ? 'ring-2 ring-gray-300' : ''
}`}
onClick={() => {
setSelectedSet(set.id)
setActiveTab('all-cards')
}}
>
<div className={`${set.isDefault ? 'bg-slate-700' : set.color} text-white p-4 rounded-t-lg`}>
<div className="flex items-center space-x-2">
{set.isDefault && <span>📂</span>}
<h4 className="font-semibold text-lg">
{set.name}
{set.isDefault && <span className="text-xs ml-2 opacity-75">()</span>}
</h4>
</div>
<p className="text-sm opacity-90">{set.description}</p>
</div>
<div className="p-4 bg-white rounded-b-lg">
<div className="flex justify-between items-center text-sm text-gray-600">
<span>{set.cardCount} </span>
<span>: {set.progress}%</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Favorites Tab */}
{activeTab === 'favorites' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<span className="text-yellow-500"></span>
({allCards.filter(card => card.isFavorite).length} )
</h3>
</div>
{allCards.filter(card => card.isFavorite).length === 0 ? (
<div className="text-center py-12">
<div className="text-yellow-500 text-6xl mb-4"></div>
<p className="text-gray-500 mb-4"></p>
<p className="text-sm text-gray-400"></p>
</div>
) : (
<div className="space-y-2">
{allCards.filter(card => card.isFavorite).map(card => (
<div key={card.id} className="bg-white border border-yellow-200 rounded-lg hover:shadow-md transition-all duration-200 cursor-pointer relative ring-1 ring-yellow-100">
<div
className="p-4"
onClick={() => {
alert(`即將進入「${card.word}」的詳細頁面 (開發中)`)
}}
>
{/* 收藏詞卡內容 - 與普通詞卡相同的佈局 */}
<div className="flex items-center justify-between">
<div className="absolute top-3 right-3">
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor(card.difficultyLevel || 'A1')}`}>
{card.difficultyLevel || 'A1'}
</span>
</div>
<div className="flex items-center gap-4">
<div className="w-54 h-36 bg-gray-100 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center">
<img
src={getExampleImage(card.word)}
alt={`${card.word} example`}
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
target.parentElement!.innerHTML = `
<div class="text-gray-400 text-xs text-center">
<svg class="w-6 h-6 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
例句圖
</div>
`
}}
/>
</div>
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">{card.word || '未設定'}</h3>
<span className="text-sm bg-gray-100 text-gray-700 px-2 py-1 rounded">
{card.partOfSpeech || 'unknown'}
</span>
</div>
<div className="flex items-center gap-4 mt-1">
<span className="text-lg text-gray-900 font-medium">{card.translation || '未設定'}</span>
{card.pronunciation && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{card.pronunciation}</span>
<button
onClick={(e) => {
e.stopPropagation()
console.log(`播放 ${card.word} 的發音`)
}}
className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center text-white hover:bg-blue-700 transition-colors"
>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</button>
</div>
)}
</div>
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<span>: {card.cardSet.name}</span>
<span>: {card.masteryLevel}%</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleToggleFavorite(card)}
className="p-2 text-yellow-500 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
title="取消收藏"
>
<svg className="w-4 h-4" fill="currentColor" 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>
</button>
<button
onClick={() => handleEdit(card)}
className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors"
title="編輯詞卡"
>
<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>
</button>
<button
onClick={() => handleDelete(card)}
className="p-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
title="刪除詞卡"
>
<svg className="w-4 h-4" 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>
</button>
</div>
<div className="text-gray-400">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* All Cards Tab */}
{activeTab === 'all-cards' && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold"> {filteredCards.length} </h3>
{selectedSet && (
<button
onClick={() => setSelectedSet(null)}
className="text-sm text-gray-600 hover:text-gray-900"
>
</button>
)}
</div>
{/* 未分類提醒 */}
{selectedSet && cardSets.find(set => set.id === selectedSet)?.isDefault && filteredCards.length > 15 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<div className="flex items-center space-x-2">
<span className="text-blue-600">💡</span>
<div className="flex-1">
<p className="text-blue-800 text-sm">
{filteredCards.length}
</p>
</div>
<button
onClick={() => setActiveTab('my-cards')}
className="text-blue-600 text-sm font-medium hover:text-blue-800"
>
</button>
</div>
</div>
)}
{filteredCards.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 mb-4"></p>
<Link
href="/generate"
className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary/90 transition-colors"
>
</Link>
</div>
) : (
<div className="space-y-2">
{filteredCards.map(card => (
<div key={card.id} className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all duration-200 cursor-pointer relative">
<div
className="p-4"
onClick={() => {
// TODO: 導航到詞卡詳細頁面
alert(`即將進入「${card.word}」的詳細頁面 (開發中)`)
}}
>
<div className="flex items-center justify-between">
{/* 詞卡右上角CEFR標註 */}
<div className="absolute top-3 right-3">
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor(card.difficultyLevel || 'A1')}`}>
{card.difficultyLevel || 'A1'}
</span>
</div>
{/* 左側:詞彙基本信息 */}
<div className="flex items-center gap-4">
{/* 例句圖片 - 超大尺寸 */}
<div className="w-54 h-36 bg-gray-100 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center">
<img
src={getExampleImage(card.word)}
alt={`${card.word} example`}
className="w-full h-full object-cover"
onError={(e) => {
// 圖片載入失敗時顯示佔位符
const target = e.target as HTMLImageElement
target.style.display = 'none'
target.parentElement!.innerHTML = `
<div class="text-gray-400 text-xs text-center">
<svg class="w-6 h-6 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
例句圖
</div>
`
}}
/>
</div>
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">{card.word || '未設定'}</h3>
<span className="text-sm bg-gray-100 text-gray-700 px-2 py-1 rounded">
{card.partOfSpeech || 'unknown'}
</span>
</div>
<div className="flex items-center gap-4 mt-1">
<span className="text-lg text-gray-900 font-medium">{card.translation || '未設定'}</span>
{card.pronunciation && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{card.pronunciation}</span>
<button
onClick={(e) => {
e.stopPropagation()
// TODO: 播放發音
console.log(`播放 ${card.word} 的發音`)
}}
className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center text-white hover:bg-blue-700 transition-colors"
>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</button>
</div>
)}
</div>
{/* 簡要統計 */}
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<span>: {card.cardSet.name}</span>
<span>: {card.masteryLevel}%</span>
</div>
</div>
</div>
{/* 右側:操作按鈕 */}
<div className="flex items-center gap-3">
{/* 快速操作按鈕 */}
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleToggleFavorite(card)}
className={`p-2 rounded-lg transition-colors ${
card.isFavorite
? 'text-yellow-500 hover:text-yellow-600 hover:bg-yellow-50'
: 'text-gray-400 hover:text-yellow-500 hover:bg-yellow-50'
}`}
title={card.isFavorite ? "取消收藏" : "加入收藏"}
>
<svg className="w-4 h-4" fill={card.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>
</button>
<button
onClick={() => handleEdit(card)}
className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors"
title="編輯詞卡"
>
<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>
</button>
<button
onClick={() => handleDelete(card)}
className="p-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
title="刪除詞卡"
>
<svg className="w-4 h-4" 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>
</button>
</div>
{/* 進入詳細頁面箭頭 */}
<div className="text-gray-400">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Flashcard Form Modal */}
{showForm && (
<FlashcardForm
cardSets={cardSets}
initialData={editingCard ? {
id: editingCard.id,
cardSetId: editingCard.cardSet ? cardSets.find(cs => cs.name === editingCard.cardSet.name)?.id || cardSets[0]?.id : cardSets[0]?.id,
english: editingCard.word,
chinese: editingCard.translation,
pronunciation: editingCard.pronunciation,
partOfSpeech: editingCard.partOfSpeech,
example: editingCard.example,
} : undefined}
isEdit={!!editingCard}
onSuccess={handleFormSuccess}
onCancel={() => {
setShowForm(false)
setEditingCard(null)
}}
/>
)}
</div>
)
}
export default function FlashcardsPage() {
return (
<ProtectedRoute>
<FlashcardsContent />
</ProtectedRoute>
)
}