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

294 lines
10 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 { useRouter } from 'next/navigation'
import { ProtectedRoute } from '@/components/shared/ProtectedRoute'
import { Navigation } from '@/components/shared/Navigation'
import { LoadingState } from '@/components/shared/LoadingState'
import { ErrorState } from '@/components/shared/ErrorState'
import { TabNavigation } from '@/components/shared/TabNavigation'
import { FlashcardForm } from '@/components/flashcards/FlashcardForm'
import { useToast } from '@/components/shared/Toast'
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
import { useFlashcardSearch } from '@/hooks/flashcards/useFlashcardSearch'
import { useFlashcardImageGeneration } from '@/hooks/flashcards/useFlashcardImageGeneration'
import { useFlashcardOperations } from '@/hooks/flashcards/useFlashcardOperations'
import { SearchControls } from '@/components/flashcards/SearchControls'
import { SearchResults as FlashcardSearchResults } from '@/components/flashcards/SearchResults'
import { PaginationControls as SharedPaginationControls } from '@/components/shared/PaginationControls'
import { getCEFRColor } from '@/lib/utils/flashcardUtils'
// 重構後的FlashcardsContent組件
function FlashcardsContent({ showForm, setShowForm }: { showForm: boolean; setShowForm: (show: boolean) => void }) {
const router = useRouter()
const toast = useToast()
const [activeTab, setActiveTab] = useState<'all-cards' | 'favorites'>('all-cards')
const [showAdvancedSearch, setShowAdvancedSearch] = useState(false)
const [totalCounts, setTotalCounts] = useState({ all: 0, favorites: 0 })
// 使用新的搜尋Hook
const [searchState, searchActions] = useFlashcardSearch(activeTab)
// 使用新的圖片生成Hook
const { generatingCards, generationProgress, generateImage } = useFlashcardImageGeneration()
// 使用新的操作Hook
const { handleEdit, handleDelete, handleToggleFavorite } = useFlashcardOperations()
// 例句圖片邏輯 - 使用 API 資料
const getExampleImage = (card: Flashcard): string | null => {
return card.primaryImageUrl || null
}
// 檢查詞彙是否有例句圖片 - 使用 API 資料
const hasExampleImage = (card: Flashcard): boolean => {
return card.hasExampleImage
}
// 圖片生成處理函數
const handleGenerateExampleImage = async (card: Flashcard) => {
await generateImage(card, async () => {
await searchActions.refetch()
await loadTotalCounts()
})
}
// 初始化數據載入
useEffect(() => {
loadTotalCounts()
}, [])
// 載入總數統計
const loadTotalCounts = async () => {
try {
// 載入所有詞卡數量
const allResult = await flashcardsService.getFlashcards()
const allCount = allResult.success && allResult.data ? allResult.data.count : 0
// 載入收藏詞卡數量
const favoritesResult = await flashcardsService.getFlashcards(undefined, true)
const favoritesCount = favoritesResult.success && favoritesResult.data ? favoritesResult.data.count : 0
setTotalCounts({ all: allCount, favorites: favoritesCount })
} catch (err) {
console.error('載入統計失敗:', err)
}
}
// 操作處理函數
const handleEditCard = (card: Flashcard) => {
handleEdit(card)
}
const handleDeleteCard = async (card: Flashcard) => {
await handleDelete(card, async () => {
await searchActions.refetch()
await loadTotalCounts()
})
}
const handleToggleFavoriteCard = async (card: Flashcard) => {
await handleToggleFavorite(card, async () => {
await searchActions.refetch()
await loadTotalCounts()
})
}
// 搜尋結果高亮函數
const highlightSearchTerm = (text: string, searchTerm: string) => {
if (!searchTerm || !text) return text
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
const parts = text.split(regex)
return parts.map((part, index) =>
regex.test(part) ? (
<mark key={index} className="bg-yellow-200 text-yellow-900 px-1 rounded">
{part}
</mark>
) : (
part
)
)
}
// 載入狀態處理
if (searchState.loading && searchState.isInitialLoad) {
return <LoadingState message="載入詞卡資料中..." />
}
if (searchState.error) {
return <ErrorState error={searchState.error} />
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* 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 flex-col sm:flex-row sm:justify-between sm:items-center mb-6 gap-4 sm:gap-0">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900"></h1>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 sm:gap-4">
<button
onClick={() => setShowForm(true)}
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors text-center"
>
</button>
<Link
href="/generate"
className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary/90 transition-colors text-center"
>
AI
</Link>
</div>
</div>
{/* Tabs */}
<TabNavigation
items={[
{
key: 'all-cards',
label: '所有詞卡',
count: totalCounts.all,
icon: '📚'
},
{
key: 'favorites',
label: '收藏詞卡',
count: totalCounts.favorites,
icon: '⭐'
}
]}
activeTab={activeTab}
onTabChange={(key) => setActiveTab(key as 'all-cards' | 'favorites')}
className="mb-6"
/>
{/* Search Controls */}
<SearchControls
searchState={searchState}
searchActions={searchActions}
showAdvancedSearch={showAdvancedSearch}
setShowAdvancedSearch={setShowAdvancedSearch}
/>
{/* 詞卡數目統計 */}
<div className="flex justify-end items-center mb-4">
<h3 className="text-lg font-semibold">
{searchState.pagination.totalCount}
{searchState.pagination.totalPages > 1 && (
<span className="text-sm font-normal text-gray-600 ml-2">
( {searchState.pagination.currentPage} {searchState.flashcards.length} )
</span>
)}
</h3>
</div>
{/* Search Results */}
<FlashcardSearchResults
searchState={searchState}
activeTab={activeTab}
onEdit={handleEditCard}
onDelete={handleDeleteCard}
onToggleFavorite={handleToggleFavoriteCard}
getCEFRColor={getCEFRColor}
highlightSearchTerm={highlightSearchTerm}
getExampleImage={getExampleImage}
hasExampleImage={hasExampleImage}
onGenerateExampleImage={handleGenerateExampleImage}
generatingCards={generatingCards}
generationProgress={generationProgress}
router={router}
/>
{/* Pagination Controls */}
<SharedPaginationControls
currentPage={searchState.pagination.currentPage}
totalPages={searchState.pagination.totalPages}
pageSize={searchState.pagination.pageSize}
totalCount={searchState.pagination.totalCount}
hasNext={searchState.pagination.hasNext}
hasPrev={searchState.pagination.hasPrev}
onPageChange={searchActions.goToPage}
onPageSizeChange={searchActions.changePageSize}
/>
</div>
{/* Toast */}
<toast.ToastContainer />
</div>
)
}
export default function FlashcardsPage() {
const [showForm, setShowForm] = useState(false)
return (
<ProtectedRoute>
<FlashcardsContent showForm={showForm} setShowForm={setShowForm} />
{/* 全域模態框 - 在最外層 */}
{showForm && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 999999
}}
onClick={() => setShowForm(false)}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '32px',
maxWidth: '600px',
width: '90%',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)'
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h2 style={{ fontSize: '24px', fontWeight: 'bold' }}></h2>
<button
onClick={() => setShowForm(false)}
style={{ fontSize: '24px', color: '#666', background: 'none', border: 'none', cursor: 'pointer' }}
>
</button>
</div>
<FlashcardForm
onSuccess={() => {
console.log('詞卡創建成功');
setShowForm(false);
// TODO: 刷新詞卡列表
}}
onCancel={() => setShowForm(false)}
/>
</div>
</div>
)}
</ProtectedRoute>
)
}