deckhearth/lib/use-admin-catalog.js

320 lines
8.3 KiB
JavaScript
Raw Normal View History

import { useState, useEffect, useRef, useCallback } from 'react';
export function useAdminCatalog() {
const [cards, setCards] = useState([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedGame, setSelectedGame] = useState('all');
const [selectedRarity, setSelectedRarity] = useState('all');
const [selectedSet, setSelectedSet] = useState('all');
const [minPrice, setMinPrice] = useState('');
const [maxPrice, setMaxPrice] = useState('');
const [verifiedOnly, setVerifiedOnly] = useState(false);
const [sortColumn, setSortColumn] = useState('name');
const [sortDirection, setSortDirection] = useState('asc');
const [pagination, setPagination] = useState({
page: 1,
limit: 50,
total: 0,
pages: 0
});
const [filters, setFilters] = useState({
games: [],
rarities: [],
sets: []
});
const [hasMore, setHasMore] = useState(true);
const [error, setError] = useState(null);
const loadingMoreRef = useRef(false);
const hasMoreRef = useRef(true);
const abortControllerRef = useRef(null);
// Batch selection state
const [selectedCardIds, setSelectedCardIds] = useState(new Set());
const [selectAllPages, setSelectAllPages] = useState(false);
const [batchEditModalOpen, setBatchEditModalOpen] = useState(false);
const [batchEditLoading, setBatchEditLoading] = useState(false);
const [batchEditError, setBatchEditError] = useState(null);
const fetchCards = async (isLoadMore = false) => {
if (isLoadMore && (loadingMoreRef.current || !hasMoreRef.current)) return;
let signal;
try {
if (isLoadMore) {
setLoadingMore(true);
loadingMoreRef.current = true;
} else {
setLoading(true);
setError(null);
}
abortControllerRef.current = new AbortController();
({ signal } = abortControllerRef.current);
const params = new URLSearchParams({
query: searchQuery,
game: selectedGame,
rarity: selectedRarity,
set: selectedSet,
minPrice,
maxPrice,
page: isLoadMore ? pagination.page + 1 : 1,
limit: pagination.limit,
sort: sortColumn,
dir: sortDirection,
verifiedOnly: verifiedOnly.toString()
});
const response = await fetch(`/api/admin/cards?${params}`, {
signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (signal.aborted) return;
if (data.success) {
const newCards = data.cards;
if (isLoadMore) {
setCards(prev => [...prev, ...newCards]);
} else {
setCards(newCards);
}
setPagination(data.pagination);
setFilters(data.filters);
setHasMore(data.pagination.hasMore);
hasMoreRef.current = data.pagination.hasMore;
} else {
throw new Error(data.error || 'Failed to fetch cards');
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Error fetching admin cards:', err);
setError(err.message);
}
} finally {
if (!signal?.aborted) {
setLoading(false);
setLoadingMore(false);
loadingMoreRef.current = false;
}
}
};
const handleSearchChange = (value) => {
setSearchQuery(value);
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
};
const handleGameChange = (value) => {
setSelectedGame(value);
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
};
const handleRarityChange = (value) => {
setSelectedRarity(value);
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
};
const handleSetChange = (value) => {
setSelectedSet(value);
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
};
const handleMinPriceChange = (value) => {
setMinPrice(value);
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
};
const handleMaxPriceChange = (value) => {
setMaxPrice(value);
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
};
const handleVerifiedChange = (value) => {
setVerifiedOnly(value);
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
};
const handleSort = (column) => {
if (sortColumn === column) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortColumn(column);
setSortDirection('asc');
}
};
const handleLoadMore = () => {
if (!loadingMore && hasMore) {
fetchCards(true);
}
};
const handleRefresh = () => {
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
fetchCards(false);
};
const handleLimitChange = (limit) => {
setPagination(prev => ({ ...prev, limit, page: 1 }));
setCards([]);
};
// Selection handlers
const handleToggleSelect = useCallback((cardId) => {
setSelectedCardIds(prev => {
const next = new Set(prev);
if (next.has(cardId)) {
next.delete(cardId);
} else {
next.add(cardId);
}
return next;
});
}, []);
const handleSelectAll = useCallback(() => {
if (selectAllPages) {
setSelectAllPages(false);
setSelectedCardIds(new Set());
} else {
// Select all cards in current view
const ids = new Set(cards.map(c => c.id));
setSelectedCardIds(ids);
setSelectAllPages(false); // Only current page by default
}
}, [cards, selectAllPages]);
const handleClearSelection = useCallback(() => {
setSelectedCardIds(new Set());
setSelectAllPages(false);
}, []);
const isSelected = useCallback((cardId) => selectedCardIds.has(cardId), [selectedCardIds]);
const selectedCount = selectedCardIds.size;
// Batch edit handlers
const openBatchEditModal = useCallback(() => {
if (selectedCount > 0) {
setBatchEditModalOpen(true);
setBatchEditError(null);
}
}, [selectedCount]);
const closeBatchEditModal = useCallback(() => {
setBatchEditModalOpen(false);
setBatchEditError(null);
}, []);
const executeBatchEdit = useCallback(async (updates) => {
if (selectedCount === 0) return;
setBatchEditLoading(true);
setBatchEditError(null);
try {
const response = await fetch('/api/admin/cards/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cardIds: Array.from(selectedCardIds),
updates
})
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Batch edit failed');
// Refresh cards to reflect changes
await fetchCards(false);
closeBatchEditModal();
handleClearSelection();
} catch (err) {
console.error('Batch edit error:', err);
setBatchEditError(err.message);
} finally {
setBatchEditLoading(false);
}
}, [selectedCardIds, selectedCount, fetchCards, closeBatchEditModal, handleClearSelection]);
useEffect(() => {
fetchCards(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return {
cards,
loading,
loadingMore,
searchQuery,
selectedGame,
selectedRarity,
selectedSet,
minPrice,
maxPrice,
verifiedOnly,
sortColumn,
sortDirection,
pagination,
filters,
hasMore,
error,
handleSearchChange,
handleGameChange,
handleRarityChange,
handleSetChange,
handleMinPriceChange,
handleMaxPriceChange,
handleVerifiedChange,
handleSort,
handleLoadMore,
handleRefresh,
handleLimitChange,
// Selection
selectedCardIds,
selectAllPages,
selectedCount,
handleToggleSelect,
handleSelectAll,
handleClearSelection,
isSelected,
// Batch edit
batchEditModalOpen,
batchEditLoading,
batchEditError,
openBatchEditModal,
closeBatchEditModal,
executeBatchEdit,
setSearchQuery,
setSelectedGame,
setSelectedRarity,
setSelectedSet,
setMinPrice,
setMaxPrice,
setVerifiedOnly,
setCards,
setPagination,
setHasMore,
setFilters
};
}