dramaling-vocab-learning/frontend/hooks/useFlashcardSearch.ts

374 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.

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,
},
];
};