294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
'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-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 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>
|
||
)
|
||
} |