PR #144 (`31da384`, 2026-06-13) shipped a `ReferenceError: useFocusTrap is not defined` to production because the flat ESLint config did NOT enable the core `no-undef` rule — only `react/jsx-no-undef` (which catches undefined JSX components, not plain JS identifier references). This PR closes that gap, narrowly. ## What changes - `eslint.config.mjs`: enable `no-undef: 'error'` for source files + define the ~40 browser / Node / Vitest globals the rule needs. Hand-curated globals list (rejected pulling in the `globals` npm package for one config block). - 3 latent bugs surfaced + fixed (NOT silenced with disables): | Site | Bug | Fix | |------|-----|-----| | `components/CollectionPageView.js:238` | `onClick={toggleFavorite}` — fn defined in `lib/use-collection-view.js:269` (collection-level favorite) but missing from the hook's `return {}` | Added to hook return + component destructure | | `components/CollectionPageView.js:532` | `onTogglePublic={togglePublic}` — same pattern, fn at line 315 of the hook | Same shape: hook return + destructure | | `components/ShareModal.js:99` | `fetchInvitedUsers()` scoped inside the useEffect body but called from `handleInvite` outside | Extracted to component scope via `useCallback`; effect dep array updated | Bugs 1 + 2 broke the "Favorite collection" button and the public-toggle in the Share modal on the collection-detail page. Bug 3 broke the "refresh invitee list" path after a successful invite. None had been flagged because the operator hadn't exercised those exact flows since the relevant hooks were last refactored. - `components/ShareModal.js`: also adds an eslint-disable for `react-hooks/set-state-in-effect` on the moved `fetchInvitedUsers()` call. Matches the canonical pattern in `pages/profile.js:90` — async fetch; setState fires post-resolve, not synchronously to the effect body. ## Why not pull in @eslint/js/recommended wholesale? The recommended bundle also enables `no-unused-vars`, `no-prototype-builtins`, `no-empty`, `no-cond-assign`, and ~10 others — each would generate dozens of pre-existing violations on this codebase. The right rule-by-rule sweep is the deferred `adopt-eslint-recommended-set` convoy. This PR is scoped to the one rule that would have caught PR #144's bug class. ## Test plan - [x] `npm run lint` — clean (1 pre-existing unrelated warning on `CollectionsPageView.js`'s `eslint-disable` directive — out of scope) - [x] `npm run test:run` — 25 files / 123 tests pass - [ ] CI on this PR - [ ] Post-merge: exercise the three formerly-broken paths (favorite a collection from its detail page; toggle a collection public via Share modal; invite a user and confirm the invitee list refreshes) ## Convoy doc `.convoys/enable-no-undef-eslint-rule.md` documents the surfaced bugs, D1 (no-undef only vs recommended bundle), D2 (hand-curated globals vs `globals` package), risks, and acceptance. Co-authored-by: Cursor <cursoragent@cursor.com>
545 lines
22 KiB
JavaScript
545 lines
22 KiB
JavaScript
/* eslint-disable @next/next/no-img-element -- Binder/card images use external URLs; next/image migration is out of scope. */
|
|
import Link from 'next/link';
|
|
import UploadImageModal from './UploadImageModal';
|
|
import ShareModal from './ShareModal';
|
|
import CollaboratorFacepile from './CollaboratorFacepile';
|
|
import CardItem from './CardItem';
|
|
import LoginCTA from './LoginCTA';
|
|
import { ManaCost, ColorFilterSymbol } from './ManaSymbols';
|
|
import ManaSymbolSettings from './ManaSymbolSettings';
|
|
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
|
|
import CollectionEditModal from './CollectionEditModal';
|
|
import CollectionDeleteModal from './CollectionDeleteModal';
|
|
import { Button, SearchBar } from './ui';
|
|
export default function CollectionPageView(props) {
|
|
const {
|
|
cards,
|
|
collection,
|
|
copySuccess,
|
|
editForm,
|
|
favoritedCards,
|
|
gameStats,
|
|
groupBy,
|
|
groupedCards,
|
|
handleAddCard,
|
|
handleAddToCollection,
|
|
handleAddToDeck,
|
|
handleDeleteCollection,
|
|
handleDownloadCSV,
|
|
handleEditCollection,
|
|
handleImageUpload,
|
|
handleSearchCards,
|
|
handleToggleFavorite,
|
|
handleToggleSelect,
|
|
identifier,
|
|
isFavorited,
|
|
loading,
|
|
router,
|
|
searchCards,
|
|
searchQuery,
|
|
searchResults,
|
|
selectedCards,
|
|
selectedRarity,
|
|
selectedTCG,
|
|
selectedType,
|
|
setCards,
|
|
setCollection,
|
|
setCopySuccess,
|
|
setEditForm,
|
|
setFavoritedCards,
|
|
setGroupBy,
|
|
setIsFavorited,
|
|
setLoading,
|
|
setSearchCards,
|
|
setSearchQuery,
|
|
setSearchResults,
|
|
setSelectedCards,
|
|
setSelectedRarity,
|
|
setSelectedTCG,
|
|
setSelectedType,
|
|
setShowDeleteModal,
|
|
setShowEditModal,
|
|
setShowSearchResults,
|
|
setShowShareModal,
|
|
setShowUploadModal,
|
|
setSortBy,
|
|
setViewMode,
|
|
showDeleteModal,
|
|
showEditModal,
|
|
showInitialLoading,
|
|
showSearchResults,
|
|
showShareModal,
|
|
showUploadModal,
|
|
sortBy,
|
|
toggleFavorite,
|
|
togglePublic,
|
|
user,
|
|
viewMode
|
|
} = props;
|
|
|
|
return (
|
|
<>
|
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
|
{/* Header */}
|
|
<div className="px-6 pt-6 pb-2">
|
|
<div className="flex items-center justify-between mb-6">
|
|
{/* Back button */}
|
|
<div className="flex items-center">
|
|
<Link href="/collections">
|
|
<button className="flex items-center text-sm font-medium hover:underline" style={{ color: 'var(--text-secondary)' }}>
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back to collections
|
|
</button>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex items-center space-x-3">
|
|
{/* Edit and Delete buttons - only show for owner and non-system collections */}
|
|
{collection.userRole === 'owner' && !collection.isSystemCollection && (
|
|
<>
|
|
<button
|
|
onClick={() => setShowEditModal(true)}
|
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover flex items-center space-x-2"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
<span>Edit</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setShowDeleteModal(true)}
|
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover flex items-center space-x-2"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
<span>Delete</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => setShowUploadModal(true)}
|
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
Upload Image
|
|
</button>
|
|
<button
|
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
🪄 Generate AI Image
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Collection Info */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
|
{collectionDisplayName(collection)}
|
|
</h1>
|
|
{/* System collection indicator */}
|
|
{collection.isSystemCollection && (
|
|
<div className="flex items-center space-x-1">
|
|
<span
|
|
className="px-2.5 py-1 text-xs font-bold rounded-full text-white shadow-sm border-2"
|
|
style={{
|
|
background:
|
|
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
|
|
borderColor: 'var(--accent-flame)',
|
|
}}
|
|
>
|
|
🔒 SYSTEM
|
|
</span>
|
|
<div className="group relative">
|
|
<svg
|
|
className="h-4 w-4 cursor-help"
|
|
style={{ color: 'var(--accent-ember)' }}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div
|
|
className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs rounded-xl shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap glass-panel-strong"
|
|
style={{ color: 'var(--text-primary)' }}
|
|
>
|
|
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-lg mb-4" style={{ color: 'var(--text-secondary)' }}>
|
|
{collection.description}
|
|
</p>
|
|
|
|
{/* TCG Tags */}
|
|
<div className="flex items-center space-x-2 mb-4">
|
|
{Object.entries(gameStats).map(([game, count]) =>
|
|
count > 0 ? (
|
|
<span
|
|
key={game}
|
|
className="px-3 py-1 text-xs font-medium rounded-full"
|
|
style={{
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
color: 'var(--text-primary)',
|
|
}}
|
|
>
|
|
{game}
|
|
</span>
|
|
) : null
|
|
)}
|
|
</div>
|
|
|
|
{/* Creator and Stats */}
|
|
<div className="flex items-center space-x-6 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
{collection.creator ? (
|
|
<CollaboratorFacepile
|
|
collectionId={identifier}
|
|
creatorEmail={collection.creator}
|
|
/>
|
|
) : (
|
|
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
Crafted by Unknown User
|
|
</span>
|
|
)}
|
|
<div>Cards: <span className="font-medium">{cards.length}</span></div>
|
|
<div>Cost: <span className="font-medium">${collection.value || '0'}</span></div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4 mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
<span>Created {new Date(collection.createdAt).toLocaleDateString()}</span>
|
|
<span>Last updated {new Date(collection.lastViewed).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Bar */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={() => setShowShareModal(true)}
|
|
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
|
|
</svg>
|
|
<span>Share</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={toggleFavorite}
|
|
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl ${
|
|
isFavorited ? '' : 'nav-item-hover'
|
|
}`}
|
|
style={
|
|
isFavorited
|
|
? {
|
|
borderColor: 'var(--accent-ember)',
|
|
color: 'var(--accent-ember)',
|
|
backgroundColor: 'transparent',
|
|
}
|
|
: { borderColor: 'var(--border)', color: 'var(--text-primary)' }
|
|
}
|
|
>
|
|
<svg className="w-4 h-4" fill={isFavorited ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
<span>Favorite</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleDownloadCSV}
|
|
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span>Download List</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-1 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
<span>Activity</span>
|
|
<span
|
|
className="px-2 py-1 rounded-full text-xs font-medium"
|
|
style={{
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
color: 'var(--text-primary)',
|
|
}}
|
|
>
|
|
123
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6">
|
|
{/* Search and Filters */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-64">
|
|
<SearchBar
|
|
value={searchCards}
|
|
onChange={(e) => {
|
|
setSearchCards(e.target.value);
|
|
handleSearchCards(e.target.value);
|
|
}}
|
|
onClear={() => {
|
|
setSearchCards('');
|
|
handleSearchCards('');
|
|
}}
|
|
placeholder="Search cards…"
|
|
/>
|
|
</div>
|
|
|
|
<select
|
|
value={selectedRarity}
|
|
onChange={(e) => setSelectedRarity(e.target.value)}
|
|
className="input-field"
|
|
>
|
|
<option>All Rarities</option>
|
|
<option>Common</option>
|
|
<option>Uncommon</option>
|
|
<option>Rare</option>
|
|
<option>Mythic</option>
|
|
<option>Legendary</option>
|
|
</select>
|
|
|
|
<select
|
|
value={selectedType}
|
|
onChange={(e) => setSelectedType(e.target.value)}
|
|
className="input-field"
|
|
>
|
|
<option>All Types</option>
|
|
<option>Creature</option>
|
|
<option>Instant</option>
|
|
<option>Sorcery</option>
|
|
<option>Artifact</option>
|
|
<option>Enchantment</option>
|
|
</select>
|
|
|
|
<select
|
|
value={groupBy}
|
|
onChange={(e) => setGroupBy(e.target.value)}
|
|
className="input-field"
|
|
>
|
|
<option>Group by Game</option>
|
|
<option>Group by Rarity</option>
|
|
<option>Group by Type</option>
|
|
</select>
|
|
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|
className="input-field"
|
|
>
|
|
<option>Sort by Name</option>
|
|
<option>Sort by Price</option>
|
|
<option>Sort by Rarity</option>
|
|
<option>Sort by Date Added</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<div
|
|
className="flex rounded-xl border"
|
|
style={{ borderColor: 'var(--border)' }}
|
|
>
|
|
<button
|
|
onClick={() => setViewMode('grid')}
|
|
className="p-2 rounded-l-xl transition-colors"
|
|
style={{
|
|
backgroundColor:
|
|
viewMode === 'grid' ? 'var(--bg-tertiary)' : 'transparent',
|
|
color:
|
|
viewMode === 'grid'
|
|
? 'var(--text-primary)'
|
|
: 'var(--text-secondary)',
|
|
}}
|
|
aria-label="Grid view"
|
|
>
|
|
<svg className="w-4 h-4" 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-r-xl transition-colors"
|
|
style={{
|
|
backgroundColor:
|
|
viewMode === 'list' ? 'var(--bg-tertiary)' : 'transparent',
|
|
color:
|
|
viewMode === 'list'
|
|
? 'var(--text-primary)'
|
|
: 'var(--text-secondary)',
|
|
}}
|
|
aria-label="List view"
|
|
>
|
|
<svg className="w-4 h-4" 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>
|
|
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => router.push('/cards')}
|
|
leadingIcon={
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
}
|
|
>
|
|
Add Cards
|
|
</Button>
|
|
|
|
<button
|
|
className="px-4 py-2 border rounded-xl nav-item-hover flex items-center space-x-2"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
<span>Scan Cards</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Game Sections */}
|
|
{Object.entries(groupedCards).map(([game, gameCards]) => (
|
|
<div key={game} className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-xl font-bold flex items-center space-x-3" style={{ color: 'var(--text-primary)' }}>
|
|
<span>{game === 'MTG' ? 'Magic The Gathering' : game}</span>
|
|
<span
|
|
className="px-2 py-1 rounded-xl text-sm font-medium"
|
|
style={{
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
color: 'var(--text-secondary)',
|
|
}}
|
|
>
|
|
{gameCards.length}
|
|
</span>
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="card-grid-container grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-3 sm:gap-4 lg:gap-6">
|
|
{gameCards.map((card, index) => (
|
|
<CardItem
|
|
key={card.id || index}
|
|
card={card}
|
|
viewMode={viewMode}
|
|
isSelected={selectedCards.some(c => c.id === card.id)}
|
|
onToggleSelect={handleToggleSelect}
|
|
onAddToCollection={handleAddToCollection}
|
|
onAddToDeck={handleAddToDeck}
|
|
onToggleFavorite={handleToggleFavorite}
|
|
isFavorited={favoritedCards.has(card.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{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)' }}>
|
|
Start Building Your List
|
|
</h3>
|
|
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
|
Add cards to get started with your collection
|
|
</p>
|
|
<Button variant="primary" onClick={() => router.push('/cards')}>
|
|
Browse Cards to Add
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Search Results Dropdown */}
|
|
{showSearchResults && searchResults.length > 0 && (
|
|
<div
|
|
className="absolute top-full left-0 right-0 glass-panel-strong rounded-xl max-h-64 overflow-y-auto z-50"
|
|
>
|
|
{searchResults.map(card => (
|
|
<div
|
|
key={card.id}
|
|
onClick={() => handleAddCard(card)}
|
|
className="flex items-center p-3 cursor-pointer nav-item-hover"
|
|
>
|
|
<img
|
|
src={card.image_url}
|
|
alt={card.name}
|
|
className="w-12 h-16 object-cover rounded mr-3"
|
|
onError={(e) => {
|
|
e.target.src = 'https://via.placeholder.com/48x64/6366f1/ffffff?text=No+Image';
|
|
}}
|
|
/>
|
|
<div>
|
|
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
|
{card.name}
|
|
</div>
|
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
{card.set_name} • ${card.market_price}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<CollectionEditModal
|
|
isOpen={showEditModal}
|
|
editForm={editForm}
|
|
setEditForm={setEditForm}
|
|
onClose={() => setShowEditModal(false)}
|
|
onSave={handleEditCollection}
|
|
/>
|
|
|
|
<CollectionDeleteModal
|
|
isOpen={showDeleteModal}
|
|
collection={collection}
|
|
onClose={() => setShowDeleteModal(false)}
|
|
onConfirm={handleDeleteCollection}
|
|
/>
|
|
|
|
{/* Upload Image Modal */}
|
|
<UploadImageModal
|
|
isOpen={showUploadModal}
|
|
onClose={() => setShowUploadModal(false)}
|
|
onUpload={handleImageUpload}
|
|
currentImage={collection.image}
|
|
/>
|
|
|
|
{/* Share Modal */}
|
|
<ShareModal
|
|
isOpen={showShareModal}
|
|
onClose={() => setShowShareModal(false)}
|
|
collectionId={identifier}
|
|
isPublic={collection.isPublic}
|
|
onTogglePublic={togglePublic}
|
|
onInviteUser={(email) => console.log('Invited:', email)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Show login CTA for non-authenticated users */}
|
|
{!user && collection?.isPublic && (
|
|
<LoginCTA message="Sign up to create your own lists and collaborate with others!" />
|
|
)}
|
|
</>
|
|
);
|
|
}
|