deckhearth/lib/use-admin-catalog.js
Randall Stillwell bf317f819e feat(admin): add admin card catalog (Phase 1)
- New admin API: /api/admin/cards with filters (game/rarity/set/price/verified),
  sorting, pagination, admin rate limiting
- New admin page: /admin/catalog with GlassSurface table, filters panel,
  sortable columns, infinite scroll pagination
- New hook: useAdminCatalog for state management
- Components: AdminCatalogView, AdminCatalogFilters, AdminCatalogTable, AdminCatalogPagination
- Protected by withAdmin middleware, reuses design system tokens
2026-09-18 14:06:01 -05:00

224 lines
No EOL
5.5 KiB
JavaScript

import { useState, useEffect, useRef } 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);
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([]);
};
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,
setSearchQuery,
setSelectedGame,
setSelectedRarity,
setSelectedSet,
setMinPrice,
setMaxPrice,
setVerifiedOnly,
setCards,
setPagination,
setHasMore,
setFilters
};
}