374 lines
10 KiB
TypeScript
374 lines
10 KiB
TypeScript
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards';
|
||
import { useDebounce } from './useDebounce';
|
||
|
||
// 類型定義
|
||
export interface SearchFilters {
|
||
search: string;
|
||
difficultyLevel: string;
|
||
partOfSpeech: string;
|
||
masteryLevel: string;
|
||
favoritesOnly: boolean;
|
||
createdAfter?: string;
|
||
createdBefore?: string;
|
||
reviewCountMin?: number;
|
||
reviewCountMax?: number;
|
||
}
|
||
|
||
export interface SortOptions {
|
||
sortBy: string;
|
||
sortOrder: 'asc' | 'desc';
|
||
}
|
||
|
||
export interface PaginationState {
|
||
currentPage: number;
|
||
pageSize: number;
|
||
totalPages: number;
|
||
totalCount: number;
|
||
hasNext: boolean;
|
||
hasPrev: boolean;
|
||
}
|
||
|
||
export interface SearchState {
|
||
// 資料
|
||
flashcards: Flashcard[];
|
||
pagination: PaginationState;
|
||
|
||
// UI 狀態
|
||
loading: boolean;
|
||
error: string | null;
|
||
isInitialLoad: boolean;
|
||
|
||
// 搜尋條件
|
||
filters: SearchFilters;
|
||
sorting: SortOptions;
|
||
|
||
// 元數據
|
||
lastUpdated: Date | null;
|
||
cacheHit: boolean;
|
||
}
|
||
|
||
export interface SearchActions {
|
||
// 篩選操作
|
||
updateFilters: (filters: Partial<SearchFilters>) => void;
|
||
clearFilters: () => void;
|
||
resetFilters: () => void;
|
||
|
||
// 排序操作
|
||
updateSorting: (sorting: Partial<SortOptions>) => void;
|
||
toggleSortOrder: () => void;
|
||
|
||
// 分頁操作
|
||
goToPage: (page: number) => void;
|
||
changePageSize: (size: number) => void;
|
||
goToNextPage: () => void;
|
||
goToPrevPage: () => void;
|
||
|
||
// 資料操作
|
||
refresh: () => Promise<void>;
|
||
refetch: () => Promise<void>;
|
||
}
|
||
|
||
// 初始狀態
|
||
const initialState: SearchState = {
|
||
flashcards: [],
|
||
pagination: {
|
||
currentPage: 1,
|
||
pageSize: 20,
|
||
totalPages: 0,
|
||
totalCount: 0,
|
||
hasNext: false,
|
||
hasPrev: false,
|
||
},
|
||
loading: false,
|
||
error: null,
|
||
isInitialLoad: true,
|
||
filters: {
|
||
search: '',
|
||
difficultyLevel: '',
|
||
partOfSpeech: '',
|
||
masteryLevel: '',
|
||
favoritesOnly: false,
|
||
},
|
||
sorting: {
|
||
sortBy: 'createdAt',
|
||
sortOrder: 'desc',
|
||
},
|
||
lastUpdated: null,
|
||
cacheHit: false,
|
||
};
|
||
|
||
export const useFlashcardSearch = (activeTab: 'all-cards' | 'favorites' = 'all-cards'): [SearchState, SearchActions] => {
|
||
const [state, setState] = useState<SearchState>(initialState);
|
||
|
||
// 搜尋邏輯
|
||
const executeSearch = useCallback(async () => {
|
||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||
|
||
try {
|
||
// 構建 API 參數
|
||
const apiParams = {
|
||
search: state.filters.search || undefined,
|
||
favoritesOnly: activeTab === 'favorites' || state.filters.favoritesOnly,
|
||
difficultyLevel: state.filters.difficultyLevel || undefined,
|
||
partOfSpeech: state.filters.partOfSpeech || undefined,
|
||
masteryLevel: state.filters.masteryLevel || undefined,
|
||
sortBy: state.sorting.sortBy,
|
||
sortOrder: state.sorting.sortOrder,
|
||
page: state.pagination.currentPage,
|
||
limit: state.pagination.pageSize,
|
||
};
|
||
|
||
// 暫時不發送difficultyLevel給後端,因為後端不支援
|
||
const result = await flashcardsService.getFlashcards(
|
||
apiParams.search,
|
||
apiParams.favoritesOnly,
|
||
undefined, // difficultyLevel 客戶端處理
|
||
apiParams.partOfSpeech,
|
||
apiParams.masteryLevel,
|
||
apiParams.sortBy,
|
||
apiParams.sortOrder,
|
||
1, // 獲取第一頁
|
||
1000 // 大數值以獲取所有資料用於客戶端篩選
|
||
);
|
||
|
||
if (result.success && result.data) {
|
||
let allFlashcards = result.data.flashcards;
|
||
|
||
// 客戶端篩選 (因為後端不支援某些篩選功能)
|
||
if (state.filters.difficultyLevel) {
|
||
allFlashcards = allFlashcards.filter(card =>
|
||
(card as any).difficultyLevel === state.filters.difficultyLevel
|
||
);
|
||
}
|
||
|
||
// 客戶端排序 (確保排序正確)
|
||
allFlashcards.sort((a, b) => {
|
||
let aValue: any, bValue: any;
|
||
|
||
switch (state.sorting.sortBy) {
|
||
case 'word':
|
||
aValue = a.word.toLowerCase();
|
||
bValue = b.word.toLowerCase();
|
||
break;
|
||
case 'createdAt':
|
||
aValue = new Date(a.createdAt);
|
||
bValue = new Date(b.createdAt);
|
||
break;
|
||
case 'masteryLevel':
|
||
aValue = a.masteryLevel;
|
||
bValue = b.masteryLevel;
|
||
break;
|
||
case 'difficultyLevel':
|
||
const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'];
|
||
aValue = levels.indexOf((a as any).difficultyLevel || 'A1');
|
||
bValue = levels.indexOf((b as any).difficultyLevel || 'A1');
|
||
break;
|
||
case 'timesReviewed':
|
||
aValue = a.timesReviewed;
|
||
bValue = b.timesReviewed;
|
||
break;
|
||
default:
|
||
aValue = new Date(a.createdAt);
|
||
bValue = new Date(b.createdAt);
|
||
}
|
||
|
||
if (state.sorting.sortOrder === 'asc') {
|
||
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
||
} else {
|
||
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
|
||
}
|
||
});
|
||
|
||
const totalFilteredCount = allFlashcards.length;
|
||
|
||
// 客戶端分頁處理
|
||
const startIndex = (state.pagination.currentPage - 1) * state.pagination.pageSize;
|
||
const endIndex = startIndex + state.pagination.pageSize;
|
||
const paginatedFlashcards = allFlashcards.slice(startIndex, endIndex);
|
||
|
||
const totalPages = Math.ceil(totalFilteredCount / state.pagination.pageSize);
|
||
const currentPage = state.pagination.currentPage;
|
||
|
||
setState(prev => ({
|
||
...prev,
|
||
flashcards: paginatedFlashcards,
|
||
pagination: {
|
||
...prev.pagination,
|
||
totalPages,
|
||
totalCount: totalFilteredCount, // 使用篩選後的總數
|
||
hasNext: currentPage < totalPages,
|
||
hasPrev: currentPage > 1,
|
||
},
|
||
loading: false,
|
||
isInitialLoad: false,
|
||
lastUpdated: new Date(),
|
||
cacheHit: false, // 暫時設為 false,等後端支援
|
||
}));
|
||
} else {
|
||
setState(prev => ({
|
||
...prev,
|
||
loading: false,
|
||
error: result.error || 'Failed to load flashcards',
|
||
}));
|
||
}
|
||
} catch (error) {
|
||
setState(prev => ({
|
||
...prev,
|
||
loading: false,
|
||
error: error instanceof Error ? error.message : 'Unknown error',
|
||
}));
|
||
}
|
||
}, [
|
||
state.filters.search,
|
||
state.filters.difficultyLevel,
|
||
state.filters.partOfSpeech,
|
||
state.filters.masteryLevel,
|
||
state.filters.favoritesOnly,
|
||
state.sorting.sortBy,
|
||
state.sorting.sortOrder,
|
||
state.pagination.currentPage,
|
||
state.pagination.pageSize,
|
||
activeTab
|
||
]);
|
||
|
||
// 防抖搜尋
|
||
const debouncedSearch = useDebounce(executeSearch, 300);
|
||
|
||
// Actions
|
||
const updateFilters = useCallback((newFilters: Partial<SearchFilters>) => {
|
||
setState(prev => ({
|
||
...prev,
|
||
filters: { ...prev.filters, ...newFilters },
|
||
pagination: { ...prev.pagination, currentPage: 1 }, // 重置頁碼
|
||
}));
|
||
}, []);
|
||
|
||
const clearFilters = useCallback(() => {
|
||
setState(prev => ({
|
||
...prev,
|
||
filters: {
|
||
search: '',
|
||
difficultyLevel: '',
|
||
partOfSpeech: '',
|
||
masteryLevel: '',
|
||
favoritesOnly: false,
|
||
},
|
||
pagination: { ...prev.pagination, currentPage: 1 },
|
||
}));
|
||
}, []);
|
||
|
||
const resetFilters = useCallback(() => {
|
||
setState(prev => ({
|
||
...prev,
|
||
filters: initialState.filters,
|
||
sorting: initialState.sorting,
|
||
pagination: { ...prev.pagination, currentPage: 1, pageSize: 20 },
|
||
}));
|
||
}, []);
|
||
|
||
const updateSorting = useCallback((newSorting: Partial<SortOptions>) => {
|
||
setState(prev => ({
|
||
...prev,
|
||
sorting: { ...prev.sorting, ...newSorting },
|
||
pagination: { ...prev.pagination, currentPage: 1 },
|
||
}));
|
||
}, []);
|
||
|
||
const toggleSortOrder = useCallback(() => {
|
||
setState(prev => ({
|
||
...prev,
|
||
sorting: {
|
||
...prev.sorting,
|
||
sortOrder: prev.sorting.sortOrder === 'asc' ? 'desc' : 'asc'
|
||
},
|
||
pagination: { ...prev.pagination, currentPage: 1 },
|
||
}));
|
||
}, []);
|
||
|
||
const goToPage = useCallback((page: number) => {
|
||
if (page >= 1 && page <= state.pagination.totalPages) {
|
||
setState(prev => ({
|
||
...prev,
|
||
pagination: { ...prev.pagination, currentPage: page },
|
||
}));
|
||
}
|
||
}, [state.pagination.totalPages]);
|
||
|
||
const changePageSize = useCallback((pageSize: number) => {
|
||
setState(prev => ({
|
||
...prev,
|
||
pagination: { ...prev.pagination, pageSize, currentPage: 1 },
|
||
}));
|
||
}, []);
|
||
|
||
const goToNextPage = useCallback(() => {
|
||
if (state.pagination.hasNext) {
|
||
goToPage(state.pagination.currentPage + 1);
|
||
}
|
||
}, [state.pagination.hasNext, state.pagination.currentPage, goToPage]);
|
||
|
||
const goToPrevPage = useCallback(() => {
|
||
if (state.pagination.hasPrev) {
|
||
goToPage(state.pagination.currentPage - 1);
|
||
}
|
||
}, [state.pagination.hasPrev, state.pagination.currentPage, goToPage]);
|
||
|
||
const refresh = useCallback(async () => {
|
||
await executeSearch();
|
||
}, [executeSearch]);
|
||
|
||
const refetch = useCallback(async () => {
|
||
setState(prev => ({ ...prev, isInitialLoad: true }));
|
||
await executeSearch();
|
||
}, [executeSearch]);
|
||
|
||
// 自動執行搜尋
|
||
useEffect(() => {
|
||
if (state.filters.search) {
|
||
debouncedSearch();
|
||
} else {
|
||
executeSearch();
|
||
}
|
||
}, [
|
||
state.filters,
|
||
state.sorting,
|
||
state.pagination.currentPage,
|
||
state.pagination.pageSize,
|
||
activeTab
|
||
]);
|
||
|
||
// 檢查是否有活動篩選
|
||
const hasActiveFilters = useMemo(() => {
|
||
return !!(
|
||
state.filters.search ||
|
||
state.filters.difficultyLevel ||
|
||
state.filters.partOfSpeech ||
|
||
state.filters.masteryLevel ||
|
||
state.filters.favoritesOnly
|
||
);
|
||
}, [state.filters]);
|
||
|
||
// 增強的狀態
|
||
const enhancedState = useMemo(() => ({
|
||
...state,
|
||
hasActiveFilters,
|
||
}), [state, hasActiveFilters]);
|
||
|
||
return [
|
||
enhancedState,
|
||
{
|
||
updateFilters,
|
||
clearFilters,
|
||
resetFilters,
|
||
updateSorting,
|
||
toggleSortOrder,
|
||
goToPage,
|
||
changePageSize,
|
||
goToNextPage,
|
||
goToPrevPage,
|
||
refresh,
|
||
refetch,
|
||
},
|
||
];
|
||
}; |