2026-06-03 13:49:25 -04:00
|
|
|
|
import CardItem from './CardItem';
|
|
|
|
|
|
import BulkSelectionToolbar from './BulkSelectionToolbar';
|
|
|
|
|
|
import CollectionSelectionModal from './CollectionSelectionModal';
|
|
|
|
|
|
|
|
|
|
|
|
export default function CardsPageView(props) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
cards,
|
|
|
|
|
|
loadingMore,
|
|
|
|
|
|
searchQuery,
|
|
|
|
|
|
selectedTCG,
|
|
|
|
|
|
setSelectedTCG,
|
|
|
|
|
|
selectedRarity,
|
|
|
|
|
|
setSelectedRarity,
|
|
|
|
|
|
selectedSet,
|
|
|
|
|
|
setSelectedSet,
|
|
|
|
|
|
selectedValueRange,
|
|
|
|
|
|
setSelectedValueRange,
|
|
|
|
|
|
viewMode,
|
|
|
|
|
|
setViewMode,
|
|
|
|
|
|
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,
|
|
|
|
|
|
} = props;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Header */}
|
2026-06-04 12:10:46 -04:00
|
|
|
|
<div className="px-4 sm:px-6 pt-6 pb-2">
|
2026-06-03 13:49:25 -04:00
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Cards
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
Browse and manage your card collection
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex space-x-2 sm:space-x-4">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setViewMode('grid')}
|
|
|
|
|
|
className={`p-2 rounded-xl transition-all duration-200 ${
|
|
|
|
|
|
viewMode === 'grid'
|
|
|
|
|
|
? 'shadow-lg'
|
|
|
|
|
|
: 'hover:shadow-md'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: viewMode === 'grid' ? 'var(--bg-tertiary)' : 'transparent',
|
|
|
|
|
|
color: viewMode === 'grid' ? 'var(--text-primary)' : 'var(--text-secondary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setViewMode('list')}
|
|
|
|
|
|
className={`p-2 rounded-xl transition-all duration-200 ${
|
|
|
|
|
|
viewMode === 'list'
|
|
|
|
|
|
? 'shadow-lg'
|
|
|
|
|
|
: 'hover:shadow-md'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: viewMode === 'list' ? 'var(--bg-tertiary)' : 'transparent',
|
|
|
|
|
|
color: viewMode === 'list' ? 'var(--text-primary)' : 'var(--text-secondary)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Quick Filters */}
|
feat(design-system): sweep authenticated body-content panels to glass (#97)
PR #95/#96 shipped the Liquid Glass foundation (tokens, primitives, gates)
plus Layout shell, modals, landing, auth pages, and form CTAs — but body-
content panels on authenticated pages (admin Card Editor, admin Card
Import, admin Submissions, dashboard, my-cards, settings, scanner panels,
card detail price cards, popovers) were still rendering as flat
var(--bg-secondary) cards. Result: the admin Tools screen and several
core pages looked unchanged after the redesign.
This sweep adds a `.glass-panel` / `.glass-panel-strong` utility
(<GlassSurface tint=mid/high rim=subtle elevation=ambient/pronounced
blur=mid/high> in class form) and applies it across 18 surfaces:
* Admin Card Editor view, search panel, form (5 sections), preview
* Admin Card Import navigation + 3 body cards + sync panel
* Admin Card Submissions list items
* Dashboard stat cards + empty-state + grid items (5 surfaces)
* My-cards empty-state CTA card
* Settings panels (3)
* Scanner page settings + grid + queue + bulk toolbar + dialog
* Scanner destination picker + camera status banner + disambiguation
* Card detail price cards (Current / TCGPlayer / CardKingdom)
* Permission indicator tooltips
* Collections page header card
* Card detail view price cards
Also migrates the lingering admin Card Editor "Card Editor / Card Import"
nav buttons and the "Save Changes" / "Import Cards" / "Run catalog sync"
CTAs to the <Button> primitive (consistent loading + disabled states).
Page header bands (full-bleed strips with border-bottom on dashboard,
my-cards, cards, collections, community/collections, collection/[id])
are intentionally left solid — they're not card-shaped surfaces and
stacking glass-on-glass directly below the already-glass topbar would
muddy the hierarchy.
Tests: lint clean, vitest 104/104, build green. The visual diff
baseline will need refresh because the homepage spec is unaffected
(it targets the unauthenticated landing page) but the dashboard/
admin/scanner surfaces will diff if/when we add baselines for them.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 22:28:52 -04:00
|
|
|
|
<div className="glass-panel p-4 sm:p-6">
|
2026-06-03 13:49:25 -04:00
|
|
|
|
<div className="mb-4">
|
|
|
|
|
|
<h3 className="text-base sm:text-lg font-semibold mb-3" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Quick Filters
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2 sm:gap-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setSelectedTCG('all')}
|
|
|
|
|
|
className={`px-4 py-2 rounded-xl font-medium transition-all duration-200 ${
|
|
|
|
|
|
selectedTCG === 'all'
|
|
|
|
|
|
? 'shadow-lg'
|
|
|
|
|
|
: 'hover:shadow-md'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: selectedTCG === 'all' ? 'var(--bg-tertiary)' : 'transparent',
|
|
|
|
|
|
color: selectedTCG === 'all' ? 'var(--text-primary)' : 'var(--text-secondary)',
|
|
|
|
|
|
border: '1px solid var(--border)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
All Games
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{tcgOptions.map(option => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={option.value}
|
|
|
|
|
|
onClick={() => setSelectedTCG(option.value)}
|
|
|
|
|
|
className={`px-4 py-2 rounded-xl font-medium transition-all duration-200 flex items-center gap-2 ${
|
|
|
|
|
|
selectedTCG === option.value
|
|
|
|
|
|
? 'shadow-lg'
|
|
|
|
|
|
: 'hover:shadow-md'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: selectedTCG === option.value ? 'var(--bg-tertiary)' : 'transparent',
|
|
|
|
|
|
color: selectedTCG === option.value ? 'var(--text-primary)' : 'var(--text-secondary)',
|
|
|
|
|
|
border: '1px solid var(--border)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>{option.icon}</span>
|
|
|
|
|
|
{option.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Advanced Filters */}
|
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Search
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Search cards..."
|
|
|
|
|
|
className="search-bar w-full pr-12"
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(e) => handleSearchChange(e.target.value)}
|
|
|
|
|
|
onKeyPress={(e) => {
|
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
|
setPagination(prev => ({ ...prev, page: 1 }));
|
|
|
|
|
|
setCards([]);
|
|
|
|
|
|
setHasMore(true);
|
|
|
|
|
|
hasMoreRef.current = true;
|
|
|
|
|
|
fetchCards(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setPagination(prev => ({ ...prev, page: 1 }));
|
|
|
|
|
|
setCards([]);
|
|
|
|
|
|
setHasMore(true);
|
|
|
|
|
|
hasMoreRef.current = true;
|
|
|
|
|
|
fetchCards(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 rounded-lg hover:bg-opacity-20 transition-all duration-200"
|
|
|
|
|
|
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Rarity
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={selectedRarity}
|
|
|
|
|
|
onChange={(e) => setSelectedRarity(e.target.value)}
|
|
|
|
|
|
className="input-field w-full"
|
|
|
|
|
|
>
|
|
|
|
|
|
{rarityOptions.map(option => (
|
|
|
|
|
|
<option key={option.value} value={option.value}>
|
|
|
|
|
|
{option.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Set
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={selectedSet}
|
|
|
|
|
|
onChange={(e) => setSelectedSet(e.target.value)}
|
|
|
|
|
|
className="input-field w-full"
|
|
|
|
|
|
>
|
|
|
|
|
|
{setOptions.map(option => (
|
|
|
|
|
|
<option key={option.value} value={option.value}>
|
|
|
|
|
|
{option.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
Value Range
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={selectedValueRange}
|
|
|
|
|
|
onChange={(e) => setSelectedValueRange(e.target.value)}
|
|
|
|
|
|
className="input-field w-full"
|
|
|
|
|
|
>
|
|
|
|
|
|
{valueRangeOptions.map(option => (
|
|
|
|
|
|
<option key={option.value} value={option.value}>
|
|
|
|
|
|
{option.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Active Filters Display */}
|
|
|
|
|
|
{(selectedTCG !== 'all' || selectedRarity !== 'all' || selectedSet !== 'all' || selectedValueRange !== 'all') && (
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{selectedTCG !== 'all' && (
|
|
|
|
|
|
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
|
border: '1px solid var(--border)'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{tcgOptions.find(opt => opt.value === selectedTCG)?.label}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setSelectedTCG('all')}
|
|
|
|
|
|
className="ml-2 hover:opacity-70"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{selectedRarity !== 'all' && (
|
|
|
|
|
|
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
|
border: '1px solid var(--border)'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{rarityOptions.find(opt => opt.value === selectedRarity)?.label}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setSelectedRarity('all')}
|
|
|
|
|
|
className="ml-2 hover:opacity-70"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{selectedSet !== 'all' && (
|
|
|
|
|
|
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
|
border: '1px solid var(--border)'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{setOptions.find(opt => opt.value === selectedSet)?.label}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setSelectedSet('all')}
|
|
|
|
|
|
className="ml-2 hover:opacity-70"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{selectedValueRange !== 'all' && (
|
|
|
|
|
|
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
|
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
|
border: '1px solid var(--border)'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{valueRangeOptions.find(opt => opt.value === selectedValueRange)?.label}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setSelectedValueRange('all')}
|
|
|
|
|
|
className="ml-2 hover:opacity-70"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setSelectedTCG('all');
|
|
|
|
|
|
setSelectedRarity('all');
|
|
|
|
|
|
setSelectedSet('all');
|
|
|
|
|
|
setSelectedValueRange('all');
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="px-3 py-1 rounded-full text-sm font-medium hover:opacity-70" style={{
|
|
|
|
|
|
color: 'var(--text-accent)',
|
|
|
|
|
|
border: '1px solid var(--border)'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
Clear All
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Cards Display */}
|
|
|
|
|
|
<div className="p-6 pb-64">
|
|
|
|
|
|
{cards.length === 0 ? (
|
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
|
<div className="text-6xl mb-4">🃏</div>
|
|
|
|
|
|
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
|
No cards found
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
|
Try adjusting your search or filters
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className={viewMode === 'grid' ? 'card-grid-container grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3 sm:gap-4 lg:gap-6 p-3 sm:p-4 lg:p-6 xl:p-8' : 'space-y-4 p-6'}>
|
|
|
|
|
|
{cards.map((card) => (
|
|
|
|
|
|
<CardItem
|
|
|
|
|
|
key={card.id}
|
|
|
|
|
|
card={card}
|
|
|
|
|
|
viewMode={viewMode}
|
|
|
|
|
|
isSelected={selectedCards.some(c => c.id === card.id)}
|
|
|
|
|
|
onToggleSelect={handleToggleSelect}
|
|
|
|
|
|
onAddToCollection={(card) => handleBulkAddToCollection([card])}
|
|
|
|
|
|
onAddToDeck={handleBulkAddToDeck}
|
|
|
|
|
|
onToggleFavorite={handleToggleFavorite}
|
|
|
|
|
|
isFavorited={favoritedCards.has(card.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Infinite Scroll Loading Indicator */}
|
|
|
|
|
|
{loadingMore && (
|
|
|
|
|
|
<div className="flex justify-center py-8">
|
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2" style={{ borderColor: 'var(--text-accent)' }}></div>
|
|
|
|
|
|
<span style={{ color: 'var(--text-secondary)' }}>Loading more cards...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Infinite Scroll Sentinel */}
|
|
|
|
|
|
<div id="infinite-scroll-sentinel" className="h-4 w-full" />
|
|
|
|
|
|
|
|
|
|
|
|
{/* Load More Button */}
|
|
|
|
|
|
{hasMore && !loadingMore && (
|
|
|
|
|
|
<div className="flex justify-center py-8">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={loadMoreCards}
|
|
|
|
|
|
className="px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg hover:from-blue-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
|
|
|
|
>
|
|
|
|
|
|
Load More Cards
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* End of results indicator */}
|
|
|
|
|
|
{!hasMore && cards.length > 0 && (
|
|
|
|
|
|
<div className="flex justify-center py-8">
|
|
|
|
|
|
<span style={{ color: 'var(--text-secondary)' }}>No more cards to load</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Bulk Selection Toolbar */}
|
|
|
|
|
|
<BulkSelectionToolbar
|
|
|
|
|
|
selectedCards={selectedCards}
|
|
|
|
|
|
onClearSelection={handleClearSelection}
|
|
|
|
|
|
onAddToCollection={handleBulkAddToCollection}
|
|
|
|
|
|
onAddToDeck={handleBulkAddToDeck}
|
|
|
|
|
|
onMarkAsOwned={handleBulkMarkAsOwned}
|
|
|
|
|
|
onRemoveFromOwned={handleBulkRemoveFromOwned}
|
|
|
|
|
|
onBulkFavorite={handleBulkFavorite}
|
|
|
|
|
|
onBulkDelete={handleBulkDelete}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Collection Selection Modal */}
|
|
|
|
|
|
<CollectionSelectionModal
|
|
|
|
|
|
isOpen={showCollectionModal}
|
|
|
|
|
|
onClose={() => setShowCollectionModal(false)}
|
|
|
|
|
|
cards={cardsToAdd}
|
|
|
|
|
|
onAddToCollections={handleAddToCollections}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|