579 lines
17 KiB
JavaScript
579 lines
17 KiB
JavaScript
|
|
import { useState, useEffect, useRef } from 'react';
|
||
|
|
import { VOCAB } from './collection-vocabulary.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Card browser state: search/filters, infinite scroll, bulk selection, favorites.
|
||
|
|
*/
|
||
|
|
export function useCardsPage() {
|
||
|
|
|
||
|
|
const [cards, setCards] = useState([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
||
|
|
const [searchQuery, setSearchQuery] = useState('');
|
||
|
|
const [selectedTCG, setSelectedTCG] = useState('all');
|
||
|
|
const [selectedRarity, setSelectedRarity] = useState('all');
|
||
|
|
const [selectedSet, setSelectedSet] = useState('all');
|
||
|
|
const [selectedValueRange, setSelectedValueRange] = useState('all');
|
||
|
|
const [viewMode, setViewMode] = useState('grid'); // grid or list
|
||
|
|
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 loadingMoreRef = useRef(false);
|
||
|
|
const hasMoreRef = useRef(true);
|
||
|
|
|
||
|
|
// Bulk selection state
|
||
|
|
const [selectedCards, setSelectedCards] = useState([]);
|
||
|
|
const [favoritedCards, setFavoritedCards] = useState(new Set());
|
||
|
|
|
||
|
|
// Modal states
|
||
|
|
const [showCollectionModal, setShowCollectionModal] = useState(false);
|
||
|
|
const [cardsToAdd, setCardsToAdd] = useState([]);
|
||
|
|
|
||
|
|
// Mana symbol settings
|
||
|
|
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
|
||
|
|
|
||
|
|
// Fetch cards from database
|
||
|
|
const fetchCards = async (isLoadMore = false) => {
|
||
|
|
try {
|
||
|
|
if (isLoadMore) {
|
||
|
|
setLoadingMore(true);
|
||
|
|
loadingMoreRef.current = true;
|
||
|
|
} else {
|
||
|
|
setLoading(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
query: searchQuery,
|
||
|
|
game: selectedTCG,
|
||
|
|
rarity: selectedRarity,
|
||
|
|
set: selectedSet,
|
||
|
|
minPrice: selectedValueRange === 'under-50' ? '0' :
|
||
|
|
selectedValueRange === '50-100' ? '50' :
|
||
|
|
selectedValueRange === '100-500' ? '100' :
|
||
|
|
selectedValueRange === '500-1000' ? '500' :
|
||
|
|
selectedValueRange === 'over-1000' ? '1000' : '',
|
||
|
|
maxPrice: selectedValueRange === '50-100' ? '100' :
|
||
|
|
selectedValueRange === '100-500' ? '500' :
|
||
|
|
selectedValueRange === '500-1000' ? '1000' : '',
|
||
|
|
page: pagination.page.toString(),
|
||
|
|
limit: pagination.limit.toString()
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await fetch(`/api/cards/search?${params}`);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
|
||
|
|
|
||
|
|
if (isLoadMore) {
|
||
|
|
setCards(prevCards => [...prevCards, ...data.cards]);
|
||
|
|
} else {
|
||
|
|
setCards(data.cards);
|
||
|
|
}
|
||
|
|
setPagination(data.pagination);
|
||
|
|
setFilters(data.filters);
|
||
|
|
const hasMoreCards = data.pagination.page < data.pagination.pages;
|
||
|
|
setHasMore(hasMoreCards);
|
||
|
|
hasMoreRef.current = hasMoreCards;
|
||
|
|
} else {
|
||
|
|
console.error('Failed to fetch cards:', data.error);
|
||
|
|
if (!isLoadMore) {
|
||
|
|
setCards([]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching cards:', error);
|
||
|
|
if (!isLoadMore) {
|
||
|
|
setCards([]);
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
setLoadingMore(false);
|
||
|
|
loadingMoreRef.current = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Load user's favorited cards
|
||
|
|
const loadFavoritedCards = async () => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
if (!token) return;
|
||
|
|
|
||
|
|
const response = await fetch('/api/favorites?type=card', {
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
const favoriteIds = new Set(data.favorites.map(fav => parseInt(fav.item_id)));
|
||
|
|
setFavoritedCards(favoriteIds);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading favorited cards:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initial load
|
||
|
|
useEffect(() => {
|
||
|
|
/* eslint-disable react-hooks/set-state-in-effect -- reset list state when filters change, then fetch */
|
||
|
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||
|
|
setCards([]);
|
||
|
|
setHasMore(true);
|
||
|
|
hasMoreRef.current = true;
|
||
|
|
fetchCards(false);
|
||
|
|
loadFavoritedCards(); // Load user's favorites
|
||
|
|
/* eslint-enable react-hooks/set-state-in-effect */
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- filter-driven reload; fetchCards closes over latest search state
|
||
|
|
}, [selectedTCG, selectedRarity, selectedSet, selectedValueRange]);
|
||
|
|
|
||
|
|
// Handle search with debounce
|
||
|
|
const [searchTimeout, setSearchTimeout] = useState(null);
|
||
|
|
|
||
|
|
const handleSearchChange = (value) => {
|
||
|
|
setSearchQuery(value);
|
||
|
|
|
||
|
|
// Clear existing timeout
|
||
|
|
if (searchTimeout) {
|
||
|
|
clearTimeout(searchTimeout);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set new timeout for search
|
||
|
|
const newTimeout = setTimeout(() => {
|
||
|
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||
|
|
setCards([]);
|
||
|
|
setHasMore(true);
|
||
|
|
hasMoreRef.current = true;
|
||
|
|
fetchCards(false);
|
||
|
|
}, 500); // 500ms delay
|
||
|
|
|
||
|
|
setSearchTimeout(newTimeout);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Cleanup timeout on unmount
|
||
|
|
useEffect(() => {
|
||
|
|
return () => {
|
||
|
|
if (searchTimeout) {
|
||
|
|
clearTimeout(searchTimeout);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}, [searchTimeout]);
|
||
|
|
|
||
|
|
// Load more cards
|
||
|
|
const loadMoreCards = async () => {
|
||
|
|
|
||
|
|
|
||
|
|
if (!loadingMoreRef.current && hasMoreRef.current) {
|
||
|
|
const nextPage = pagination.page + 1;
|
||
|
|
|
||
|
|
setPagination(prev => ({ ...prev, page: nextPage }));
|
||
|
|
|
||
|
|
// Use the next page number directly in the fetch
|
||
|
|
try {
|
||
|
|
setLoadingMore(true);
|
||
|
|
loadingMoreRef.current = true;
|
||
|
|
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
query: searchQuery,
|
||
|
|
game: selectedTCG,
|
||
|
|
rarity: selectedRarity,
|
||
|
|
set: selectedSet,
|
||
|
|
minPrice: selectedValueRange === 'under-50' ? '0' :
|
||
|
|
selectedValueRange === '50-100' ? '50' :
|
||
|
|
selectedValueRange === '100-500' ? '100' :
|
||
|
|
selectedValueRange === '500-1000' ? '500' :
|
||
|
|
selectedValueRange === 'over-1000' ? '1000' : '',
|
||
|
|
maxPrice: selectedValueRange === '50-100' ? '100' :
|
||
|
|
selectedValueRange === '100-500' ? '500' :
|
||
|
|
selectedValueRange === '500-1000' ? '1000' : '',
|
||
|
|
page: nextPage.toString(),
|
||
|
|
limit: pagination.limit.toString()
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await fetch(`/api/cards/search?${params}`);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
|
||
|
|
setCards(prevCards => [...prevCards, ...data.cards]);
|
||
|
|
setPagination(data.pagination);
|
||
|
|
setFilters(data.filters);
|
||
|
|
const hasMoreCards = data.pagination.page < data.pagination.pages;
|
||
|
|
setHasMore(hasMoreCards);
|
||
|
|
hasMoreRef.current = hasMoreCards;
|
||
|
|
} else {
|
||
|
|
console.error('Failed to fetch more cards:', data.error);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching more cards:', error);
|
||
|
|
} finally {
|
||
|
|
setLoadingMore(false);
|
||
|
|
loadingMoreRef.current = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Update refs when state changes
|
||
|
|
useEffect(() => {
|
||
|
|
hasMoreRef.current = hasMore;
|
||
|
|
loadingMoreRef.current = loadingMore;
|
||
|
|
}, [hasMore, loadingMore]);
|
||
|
|
|
||
|
|
// Intersection Observer for infinite scroll
|
||
|
|
useEffect(() => {
|
||
|
|
const observer = new IntersectionObserver(
|
||
|
|
(entries) => {
|
||
|
|
entries.forEach((entry) => {
|
||
|
|
if (entry.isIntersecting && hasMoreRef.current && !loadingMoreRef.current) {
|
||
|
|
loadMoreCards();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
},
|
||
|
|
{
|
||
|
|
rootMargin: '50px',
|
||
|
|
threshold: 0.1
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
const setupObserver = () => {
|
||
|
|
const sentinel = document.getElementById('infinite-scroll-sentinel');
|
||
|
|
if (sentinel) {
|
||
|
|
observer.observe(sentinel);
|
||
|
|
} else {
|
||
|
|
setTimeout(setupObserver, 100);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
setupObserver();
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
const sentinel = document.getElementById('infinite-scroll-sentinel');
|
||
|
|
if (sentinel) {
|
||
|
|
observer.unobserve(sentinel);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- observer setup once; loadMoreCards uses refs for latest state
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const getRarityColor = (rarity) => {
|
||
|
|
// Map database rarity values to colors
|
||
|
|
const rarityColorMap = {
|
||
|
|
'common': '#6B7280',
|
||
|
|
'uncommon': '#10B981',
|
||
|
|
'rare': '#F59E0B',
|
||
|
|
'mythic': '#FFD700',
|
||
|
|
'holographic': '#FF6B6B',
|
||
|
|
'enchanted': '#A855F7',
|
||
|
|
'secret rare': '#FF6B6B',
|
||
|
|
'ultra rare': '#A855F7'
|
||
|
|
};
|
||
|
|
return rarityColorMap[rarity?.toLowerCase()] || '#6B7280';
|
||
|
|
};
|
||
|
|
|
||
|
|
const tcgOptions = [
|
||
|
|
{ value: 'MTG', label: 'Magic: The Gathering', color: 'purple', icon: '🔮' },
|
||
|
|
{ value: 'Pokemon', label: 'Pokemon', color: 'blue', icon: '⚡' },
|
||
|
|
{ value: 'Lorcana', label: 'Disney Lorcana', color: 'pink', icon: '✨' }
|
||
|
|
];
|
||
|
|
|
||
|
|
const rarityOptions = [
|
||
|
|
{ value: 'all', label: 'All Rarities' },
|
||
|
|
{ value: 'common', label: 'Common' },
|
||
|
|
{ value: 'uncommon', label: 'Uncommon' },
|
||
|
|
{ value: 'rare', label: 'Rare' },
|
||
|
|
{ value: 'mythic', label: 'Mythic' },
|
||
|
|
{ value: 'enchanted', label: 'Enchanted' },
|
||
|
|
...filters.rarities.filter(rarity =>
|
||
|
|
!['all', 'common', 'uncommon', 'rare', 'mythic', 'enchanted'].includes(rarity)
|
||
|
|
).map(rarity => ({
|
||
|
|
value: rarity,
|
||
|
|
label: rarity,
|
||
|
|
color: getRarityColor(rarity)
|
||
|
|
}))
|
||
|
|
];
|
||
|
|
|
||
|
|
const setOptions = [
|
||
|
|
{ value: 'all', label: 'All Sets' },
|
||
|
|
...filters.sets.map(set => ({
|
||
|
|
value: set,
|
||
|
|
label: set
|
||
|
|
}))
|
||
|
|
];
|
||
|
|
|
||
|
|
const valueRangeOptions = [
|
||
|
|
{ value: 'all', label: 'All Values' },
|
||
|
|
{ value: 'under-50', label: 'Under $50' },
|
||
|
|
{ value: '50-100', label: '$50 - $100' },
|
||
|
|
{ value: '100-500', label: '$100 - $500' },
|
||
|
|
{ value: '500-1000', label: '$500 - $1,000' },
|
||
|
|
{ value: 'over-1000', label: 'Over $1,000' }
|
||
|
|
];
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
const formatCurrency = (amount) => {
|
||
|
|
return new Intl.NumberFormat('en-US', {
|
||
|
|
style: 'currency',
|
||
|
|
currency: 'USD'
|
||
|
|
}).format(amount);
|
||
|
|
};
|
||
|
|
|
||
|
|
const getRarityLabel = (rarity) => {
|
||
|
|
const rarityOption = rarityOptions.find(option => option.value === rarity);
|
||
|
|
return rarityOption ? rarityOption.label : rarity;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Bulk selection handlers
|
||
|
|
const handleToggleSelect = (card) => {
|
||
|
|
setSelectedCards(prev => {
|
||
|
|
const isSelected = prev.some(c => c.id === card.id);
|
||
|
|
if (isSelected) {
|
||
|
|
return prev.filter(c => c.id !== card.id);
|
||
|
|
} else {
|
||
|
|
return [...prev, card];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleClearSelection = () => {
|
||
|
|
setSelectedCards([]);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleToggleFavorite = async (card) => {
|
||
|
|
try {
|
||
|
|
const isFavorited = favoritedCards.has(card.id);
|
||
|
|
const method = isFavorited ? 'DELETE' : 'POST';
|
||
|
|
|
||
|
|
const response = await fetch('/api/favorites', {
|
||
|
|
method,
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
itemType: 'card',
|
||
|
|
itemId: card.id
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
setFavoritedCards(prev => {
|
||
|
|
const newSet = new Set(prev);
|
||
|
|
if (isFavorited) {
|
||
|
|
newSet.delete(card.id);
|
||
|
|
} else {
|
||
|
|
newSet.add(card.id);
|
||
|
|
}
|
||
|
|
return newSet;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error toggling favorite:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Bulk action handlers
|
||
|
|
const handleBulkAddToCollection = (cards) => {
|
||
|
|
setCardsToAdd(cards);
|
||
|
|
setShowCollectionModal(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBulkAddToDeck = (cards) => {
|
||
|
|
console.log('Adding to deck:', cards);
|
||
|
|
alert(`Adding ${cards.length} cards to deck (functionality coming soon)`);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBulkMarkAsOwned = async (cards) => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
if (!token) {
|
||
|
|
alert(`Please log in to ${VOCAB.ADD_TO_MY_COLLECTION.toLowerCase()}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let successCount = 0;
|
||
|
|
for (const card of cards) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/cards/${card.id}/ownership`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ quantity: 1 })
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
successCount++;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Error marking card ${card.id} as owned:`, error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (successCount === cards.length) {
|
||
|
|
alert(`Successfully added ${cards.length} cards to ${VOCAB.MY_COLLECTION}!`);
|
||
|
|
} else {
|
||
|
|
alert(`Added ${successCount} out of ${cards.length} cards to ${VOCAB.MY_COLLECTION}. Some operations may have failed.`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear selection after successful operation
|
||
|
|
setSelectedCards([]);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error in bulk mark as owned:', error);
|
||
|
|
alert(`Failed to add cards to ${VOCAB.MY_COLLECTION}. Please try again.`);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBulkRemoveFromOwned = async (cards) => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
if (!token) {
|
||
|
|
alert(`Please log in to ${VOCAB.REMOVE_FROM_MY_COLLECTION.toLowerCase()}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let successCount = 0;
|
||
|
|
for (const card of cards) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/cards/${card.id}/ownership`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${token}`
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ quantity: 0 })
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
successCount++;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Error removing card ${card.id} from owned:`, error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (successCount === cards.length) {
|
||
|
|
alert(`Successfully removed ${cards.length} cards from ${VOCAB.MY_COLLECTION}!`);
|
||
|
|
} else {
|
||
|
|
alert(`Removed ${successCount} out of ${cards.length} cards from ${VOCAB.MY_COLLECTION}. Some operations may have failed.`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear selection after successful operation
|
||
|
|
setSelectedCards([]);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error in bulk remove from owned:', error);
|
||
|
|
alert(`Failed to remove cards from ${VOCAB.MY_COLLECTION}. Please try again.`);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBulkFavorite = async (cards) => {
|
||
|
|
try {
|
||
|
|
for (const card of cards) {
|
||
|
|
if (!favoritedCards.has(card.id)) {
|
||
|
|
await fetch('/api/favorites', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
itemType: 'card',
|
||
|
|
itemId: card.id
|
||
|
|
})
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
setFavoritedCards(prev => {
|
||
|
|
const newSet = new Set(prev);
|
||
|
|
cards.forEach(card => newSet.add(card.id));
|
||
|
|
return newSet;
|
||
|
|
});
|
||
|
|
|
||
|
|
alert(`Added ${cards.length} cards to favorites`);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error bulk favoriting:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBulkDelete = (cards) => {
|
||
|
|
console.log('Bulk delete:', cards);
|
||
|
|
alert(`Bulk delete functionality coming soon for ${cards.length} cards`);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Collection modal handlers
|
||
|
|
const handleAddToCollections = (results, selectedCollectionIds, cards) => {
|
||
|
|
const successCount = results.filter(r => r.success).length;
|
||
|
|
const totalAttempts = results.length;
|
||
|
|
|
||
|
|
if (successCount === totalAttempts) {
|
||
|
|
alert(`Successfully added ${cards.length} card${cards.length !== 1 ? 's' : ''} to ${selectedCollectionIds.length} list${selectedCollectionIds.length !== 1 ? 's' : ''}!`);
|
||
|
|
} else {
|
||
|
|
alert(`Added ${successCount} out of ${totalAttempts} cards. Some additions may have failed.`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear selection after successful addition
|
||
|
|
setSelectedCards([]);
|
||
|
|
};
|
||
|
|
|
||
|
|
const showInitialLoading = loading && cards.length === 0;
|
||
|
|
|
||
|
|
return {
|
||
|
|
cards,
|
||
|
|
loading,
|
||
|
|
loadingMore,
|
||
|
|
showInitialLoading,
|
||
|
|
searchQuery,
|
||
|
|
selectedTCG,
|
||
|
|
setSelectedTCG,
|
||
|
|
selectedRarity,
|
||
|
|
setSelectedRarity,
|
||
|
|
selectedSet,
|
||
|
|
setSelectedSet,
|
||
|
|
selectedValueRange,
|
||
|
|
setSelectedValueRange,
|
||
|
|
viewMode,
|
||
|
|
setViewMode,
|
||
|
|
filters,
|
||
|
|
hasMore,
|
||
|
|
selectedCards,
|
||
|
|
favoritedCards,
|
||
|
|
showCollectionModal,
|
||
|
|
setShowCollectionModal,
|
||
|
|
cardsToAdd,
|
||
|
|
handleSearchChange,
|
||
|
|
fetchCards,
|
||
|
|
loadMoreCards,
|
||
|
|
hasMoreRef,
|
||
|
|
setPagination,
|
||
|
|
setCards,
|
||
|
|
setHasMore,
|
||
|
|
tcgOptions,
|
||
|
|
rarityOptions,
|
||
|
|
setOptions,
|
||
|
|
valueRangeOptions,
|
||
|
|
handleToggleSelect,
|
||
|
|
handleClearSelection,
|
||
|
|
handleToggleFavorite,
|
||
|
|
handleBulkAddToCollection,
|
||
|
|
handleBulkAddToDeck,
|
||
|
|
handleBulkMarkAsOwned,
|
||
|
|
handleBulkRemoveFromOwned,
|
||
|
|
handleBulkFavorite,
|
||
|
|
handleBulkDelete,
|
||
|
|
handleAddToCollections,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|