deckhearth/components/CardsPageView.js
varutasu 22364de8c9
refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive (#117) (#117)
Comprehensive design sweep across the rest of the app following the
shipped Liquid Glass + corner-border-light system (#116).

## Three classes of finding

### 1. Broken Tailwind token classes (HIGH — pages were unstyled)

The decks / deck-builder / deck-detail cluster relied on Tailwind
classes that don't exist in `tailwind.config.js` (no `bg-bg-*`,
`text-text-*`, `border-border`, `bg-accent-ember`,
`focus:ring-accent-ember`, `hover:bg-accent-ember-dark`). Those classes
produced ZERO CSS — backgrounds were transparent, borders invisible,
hover states absent.

Rewrote with inline `style={{ ... CSS vars ... }}` + the `<Button>` /
`<SearchBar>` primitives + `glass-panel` surfaces:

- `pages/decks.js` (full page)
- `pages/deck/[id].js` (header, stats sidebar, group-by controls,
  card list)
- `pages/deck-builder.js` (loading spinner)
- `components/DeckBuilderView.js` (toolbar + main panel)
- `components/DeckBuilderCardBrowser.js` (full rewrite; integrated
  `<SearchBar>` for the card-picker input)
- `components/DeckBuilderDeckList.js` (full rewrite)
- `components/DeckBuilderStatsBar.js`
- `components/ManaSymbolSettings.js`
- `components/ManaSymbols.js` (single `text-text-secondary`)
- `pages/admin/card-editor.js` cluster was already clean

### 2. Duplicative / stale page searches

Replaced raw `<input>` search controls with the `<SearchBar>` primitive
(adds clear button, ember focus ring, system-consistent rounded
corners). Kept page-specific filter searches (they filter the visible
list — distinct from the global TopSearchBar command palette):

- `pages/my-cards.js`
- `pages/community/collections.js`
- `components/CardsPageView.js`
- `components/CollectionPageView.js`
- `components/DeckBuilderCardBrowser.js`

`pages/my-cards.js` filter wrapper also lifted into a `glass-panel`
chip instead of a solid `var(--bg-primary)` band.

### 3. Square corners + stale palette in shared views

- `components/CollectionPageView.js`: 10 action buttons (`rounded-lg`
  + `hover:bg-gray-50`) → `rounded-xl` + `nav-item-hover`; 4 filter
  selects (`focus:ring-purple-500 rounded-lg`) → `.input-field`;
  view-mode toggle (`bg-white text-gray-900` — invisible in dark mode)
  → tokenised; SYSTEM badge gradient (`from-blue-500 to-purple-600`)
  → ember↔flame; tooltip (`bg-gray-900`) → `glass-panel-strong`;
  search-results dropdown (`bg-white border-gray-200` — invisible in
  dark mode) → `glass-panel-strong`; Activity / game-count /
  TCG-game badges palette-aligned.
- `components/CardsPageView.js`: "Load More Cards" button
  (`bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg`) →
  `<Button variant="primary" size="lg">`.
- `components/CollectionsPageView.js`: matching SYSTEM badge +
  tooltip cleanup.
- `components/ShareModal.js`: user-search dropdown
  (`border-gray-200 hover:bg-gray-50`) and email-invite card moved
  onto `glass-panel` + `nav-item-hover`; social-share buttons
  `rounded-lg hover:bg-gray-50` → `rounded-xl nav-item-hover`.
- `components/Layout.js`: profile-menu dropdown row
  (`hover:bg-gray-50 dark:hover:bg-gray-700`) → `nav-item-hover`.
- `components/CardItem.js`: bulk-select checkbox
  `focus:ring-purple-500` → ember.

### 4. `dark:` modifier classes (broken with `[data-theme]` theming)

This app uses `[data-theme="dark"]` CSS selector theming, not
Tailwind's `class` strategy, so `dark:bg-green-900/20` etc. produced
no CSS in dark mode. Affected alerts on `pages/settings.js` and
`pages/profile.js` — replaced with `glass-panel` + semantic border
colour (flame for success, #dc2626 for error).

`pages/settings.js` sidebar nav also moved off its hardcoded full-ember
fill onto the system `nav-item` / `nav-item-active` / `nav-item-hover`
pattern for consistency with the global sidebar.

## Verification

- `npm run build` — green (Next 16 + Turbopack)
- `npm run lint` — 0 errors, 1 unrelated pre-existing warning
- `npm run test:run` — 113/113 pass (no test changes needed)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:06:22 -05:00

390 lines
15 KiB
JavaScript
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 CardItem from './CardItem';
import BulkSelectionToolbar from './BulkSelectionToolbar';
import CollectionSelectionModal from './CollectionSelectionModal';
import { Button, SearchBar } from './ui';
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 */}
<div className="px-4 sm:px-6 pt-6 pb-2">
<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 */}
<div className="glass-panel p-4 sm:p-6">
<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>
<SearchBar
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
onClear={() => {
handleSearchChange('');
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
}
}}
placeholder="Search cards…"
/>
</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 variant="primary" size="lg" onClick={loadMoreCards}>
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}
/>
</>
);
}