deckhearth/pages/scanner.js
varutasu 0668b0c7d0
refactor(auth): collapse lib/auth-context.js + lib/admin-auth.js onto lib/use-auth.js (#31)
`lib/use-auth.js` is now the sole client-side auth surface (P1 §9 of
`.convoys/ship-readiness.md`). The legacy `lib/auth-context.js`
(`AuthProvider` + `useAuth`) and `lib/admin-auth.js` (`AdminProvider` +
`useAdmin` + `useIsAdmin`) are deleted; every importer is migrated to
the canonical hook. Pre-convoy a worst-case page mount issued THREE
identical `GET /api/auth/verify` requests (one per provider/hook); the
post-convoy floor is one verify per page mount (3 → 1 on
`pages/card/[id].js`, 2 → 1 elsewhere).

Importer inventory swept (7 source files):
- `pages/_app.js` — removed `<AuthProvider>` wrapper; `<ThemeProvider>`
  is now the only top-level provider. `lib/use-auth.js` is hook-only,
  no replacement provider needed.
- `pages/index.js`, `pages/scanner.js`, `pages/decks.js`,
  `pages/deck/[id].js`, `pages/deck-builder.js` — `import { useAuth }`
  path swap from `../lib/auth-context` to `../lib/use-auth`. All five
  pages destructured only `{ user }` or `{ user, loading }`; verified
  no consumer reads `login` / `register` from useAuth (those flows are
  in `pages/login.js` / `pages/signup.js` which call the API directly),
  so no shape-parity gap on `lib/use-auth.js`.
- `pages/card/[id].js` — replaced `useIsAdmin()` (the only consumer of
  `lib/admin-auth.js` anywhere in the tree) with synchronous
  `user?.role === 'admin'` derived from the existing `useAuth()` call.
  Render condition at line 524 stays byte-identical.

Decisions documented in `.convoys/single-auth-provider.md`:
- D1: no extension to `lib/use-auth.js` (zero call sites for `login` /
  `register` from useAuth — those flows are direct fetches in
  `login.js` / `signup.js`).
- D2: `useIsAdmin()` collapses onto `useAuth()`; no separate hook.
- D3: provider tree `<ThemeProvider><AuthProvider>{children}</AuthProvider></ThemeProvider>`
  → `<ThemeProvider>{children}</ThemeProvider>`.
- D4: 3 → 1 verify roundtrip on `card/[id].js`; 2 → 1 on every other
  page-load.
- D5: zero test files modified; the 21-test vitest suite is server-
  side or prop-driven (`Layout.test.js` passes `user` as a prop, never
  imports the legacy hooks).

Doc / config updates so the deletion lands cleanly:
- `.github/CODEOWNERS` — drop the two CODEOWNERS lines for the deleted
  files.
- `AGENTS.md` § 2 architecture row + § 3 "Auth (client)" bullet —
  rewritten for the post-convoy single-surface state.
- `.cursor/rules/auth-and-permissions.mdc` — § "Legacy" reframed to
  "deleted by this convoy"; § "Authentication state on the client"
  updated to the post-convoy `useAuth()` shape and the direct-fetch
  login flow used by `login.js` / `signup.js`.
- `.cursor/rules/no-go-zones.mdc` — auth-refactors bullet drops the
  deleted files from the canonical list.
- `.cursor/skills/add-page/SKILL.md` — checklist + anti-pattern row
  refer to the deletion.

Verification:
- `rg "lib/auth-context|lib/admin-auth" --type js` → 0 hits in source.
- `npm run lint` → 128 → 125 problems (3 fewer errors from the deleted
  unused-import lines; no regression).
- `npm run test:run` → 21/21 pass (including the 5 Layout regression
  locks from `fix-layout-default-user`, which are prop-driven and
  unaffected).
- `npm run build` → all 26 pages compile end-to-end; no SSR / static-
  generation breakage that would have surfaced if a page tried to use
  the legacy context hook unwrapped.
- Manual smoke deferred to operator post-merge per convoy doc.

Risks (full discussion in convoy file):
- R1 shape parity gap — verified zero consumers of legacy-only
  surface; mitigated.
- R2 SSR mismatch from removing `<AuthProvider>` — `useEffect`-
  guarded `localStorage` read; identical SSR shape pre/post; build
  passes.
- R3 missed importer — post-delete grep + build pass would surface
  any miss.
- R5 stale `useAuth` cache across components — pre-existing
  pattern, called out as follow-up rather than addressed here.

Out of scope: any change to `lib/permission-middleware.js` (server-
side; resolved P0 #1), `lib/auth-secret.js` (resolved P0 #2),
`pages/api/**` route handlers, login / register API contracts, or
the seeded admin account flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:58:08 -05:00

776 lines
No EOL
31 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Layout from '../components/Layout';
import CameraScanner from '../components/CameraScanner';
import OCRSettings from '../components/OCRSettings';
import { ManaCost, ColorIdentity } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth';
export default function Scanner() {
const { user } = useAuth();
const router = useRouter();
const [scannedCards, setScannedCards] = useState([]);
const [collections, setCollections] = useState([]);
const [decks, setDecks] = useState([]);
const [showCreateCollection, setShowCreateCollection] = useState(false);
const [newCollectionName, setNewCollectionName] = useState('');
const [showOCRSettings, setShowOCRSettings] = useState(false);
// Bulk action states
const [selectedCards, setSelectedCards] = useState(new Set());
const [bulkAction, setBulkAction] = useState(''); // 'owned', 'collection', 'deck'
const [bulkTarget, setBulkTarget] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
// Mana symbol settings
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
// Redirect to login if not authenticated
useEffect(() => {
if (!user) {
router.push('/login');
return;
}
}, [user, router]);
// Load collections and decks
useEffect(() => {
if (user) {
loadCollections();
loadDecks();
}
}, [user]);
const loadCollections = async () => {
try {
const response = await fetch('/api/collections', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
if (response.ok) {
const data = await response.json();
// Filter out system collections (like "All My Cards")
const userCollections = data.filter(collection => !collection.is_system_collection);
setCollections(userCollections);
}
} catch (error) {
console.error('Error loading collections:', error);
}
};
const loadDecks = async () => {
try {
const response = await fetch('/api/decks', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
if (response.ok) {
const data = await response.json();
setDecks(data);
}
} catch (error) {
console.error('Error loading decks:', error);
}
};
const handleCardScanned = async (cardData) => {
console.log('Card scanned:', cardData);
console.log('Looking for existing card with name:', cardData.name, 'set:', cardData.set);
// Check if this card already exists in the queue
setScannedCards(prev => {
console.log('Current queue:', prev.map(c => ({ name: c.name, set: c.set, processed: c.processed })));
const existingCardIndex = prev.findIndex(existing =>
existing.name === cardData.name &&
existing.set === cardData.set &&
!existing.processed
);
if (existingCardIndex !== -1) {
console.log(`📈 Incrementing quantity for existing card: ${cardData.name}`);
// Increment quantity of existing card
const updatedCards = [...prev];
updatedCards[existingCardIndex] = {
...updatedCards[existingCardIndex],
quantity: (updatedCards[existingCardIndex].quantity || 1) + 1,
timestamp: new Date().toISOString() // Update timestamp
};
return updatedCards;
} else {
console.log(`🆕 Adding new card to queue: ${cardData.name}`);
// Add new card to queue
const scannedCard = {
...cardData,
id: Date.now() + Math.random(), // More unique ID for the queue
name: cardData.name,
set: cardData.set,
quantity: 1,
timestamp: new Date().toISOString(),
processed: false
};
return [scannedCard, ...prev];
}
});
};
// Quantity management functions
const incrementCardQuantity = (cardId) => {
setScannedCards(prev => prev.map(card =>
card.id === cardId
? { ...card, quantity: (card.quantity || 1) + 1 }
: card
));
};
const decrementCardQuantity = (cardId) => {
setScannedCards(prev => prev.map(card =>
card.id === cardId
? { ...card, quantity: Math.max(1, (card.quantity || 1) - 1) }
: card
));
};
const handleError = (error) => {
console.error('Scanner error:', error);
// You could show a toast notification here
};
// Individual card actions
const addSingleCardToOwned = async (card) => {
try {
await addToOwnedCards(card);
markCardAsProcessed(card.id, 'owned');
} catch (error) {
console.error('Error adding card to owned:', error);
}
};
const addSingleCardToCollection = async (card, collectionId) => {
try {
await addToCollection(card, collectionId);
markCardAsProcessed(card.id, 'collection');
} catch (error) {
console.error('Error adding card to collection:', error);
}
};
const addSingleCardToDeck = async (card, deckId) => {
try {
await addToDeck(card, deckId);
markCardAsProcessed(card.id, 'deck');
} catch (error) {
console.error('Error adding card to deck:', error);
}
};
// Bulk actions
const handleBulkAction = async () => {
if (!bulkAction || selectedCards.size === 0) return;
setIsProcessing(true);
try {
const cardsToProcess = scannedCards.filter(card => selectedCards.has(card.id));
for (const card of cardsToProcess) {
if (bulkAction === 'owned') {
await addToOwnedCards(card);
} else if (bulkAction === 'collection' && bulkTarget) {
await addToCollection(card, bulkTarget);
} else if (bulkAction === 'deck' && bulkTarget) {
await addToDeck(card, bulkTarget);
}
markCardAsProcessed(card.id, bulkAction);
}
// Clear selections and reset bulk action state
setSelectedCards(new Set());
setBulkAction('');
setBulkTarget('');
} catch (error) {
console.error('Error processing bulk action:', error);
} finally {
setIsProcessing(false);
}
};
const markCardAsProcessed = (cardId, action) => {
setScannedCards(prev => prev.map(card =>
card.id === cardId
? { ...card, processed: true, processedAction: action }
: card
));
};
// Helper functions for API calls
const addToOwnedCards = async (cardData) => {
const response = await fetch('/api/user-cards', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
cardId: cardData.databaseId,
quantity: 1,
condition: 'NM'
})
});
if (!response.ok) {
throw new Error('Failed to add to owned cards');
}
};
const addToCollection = async (cardData, collectionId) => {
const response = await fetch(`/api/collections/${collectionId}/cards`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
cardId: cardData.databaseId,
quantity: 1
})
});
if (!response.ok) {
throw new Error('Failed to add to collection');
}
};
const addToDeck = async (cardData, deckId) => {
const response = await fetch(`/api/decks/${deckId}/cards`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
cardId: cardData.databaseId,
quantity: 1
})
});
if (!response.ok) {
throw new Error('Failed to add to deck');
}
};
const createCollection = async () => {
if (!newCollectionName.trim()) return;
try {
const response = await fetch('/api/collections', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
name: newCollectionName,
description: 'Created from card scanner',
is_public: false
})
});
if (response.ok) {
const newCollection = await response.json();
setCollections(prev => [newCollection, ...prev]);
setBulkTarget(newCollection.id.toString());
setNewCollectionName('');
setShowCreateCollection(false);
}
} catch (error) {
console.error('Error creating collection:', error);
}
};
const clearScannedCards = () => {
setScannedCards([]);
setSelectedCards(new Set());
};
const removeScannedCard = (cardId) => {
setScannedCards(prev => prev.filter(card => card.id !== cardId));
setSelectedCards(prev => {
const newSet = new Set(prev);
newSet.delete(cardId);
return newSet;
});
};
const toggleCardSelection = (cardId) => {
setSelectedCards(prev => {
const newSet = new Set(prev);
if (newSet.has(cardId)) {
newSet.delete(cardId);
} else {
newSet.add(cardId);
}
return newSet;
});
};
const selectAllCards = () => {
const unprocessedCards = scannedCards.filter(card => !card.processed);
setSelectedCards(new Set(unprocessedCards.map(card => card.id)));
};
const deselectAllCards = () => {
setSelectedCards(new Set());
};
if (!user) {
return <div>Redirecting to login...</div>;
}
return (
<Layout user={user}>
<div className="h-full flex flex-col">
{/* Header */}
<div className="px-6 pt-6 pb-4">
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
🃏 Card Scanner
</h1>
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
Scan cards to identify them, then choose what to do with your collection
</p>
</div>
{/* Main Content - Full Height */}
<div className="flex-1 grid grid-cols-1 lg:grid-cols-5 gap-6 px-6 pb-6">
{/* Camera Scanner */}
<div className="lg:col-span-3 flex flex-col">
<div className="flex-1 rounded-xl p-6 flex flex-col" style={{ backgroundColor: 'var(--bg-secondary)', border: '1px solid var(--border)' }}>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
Camera Scanner
</h2>
<button
onClick={() => setShowOCRSettings(true)}
className="px-4 py-2 rounded-xl font-medium border transition-all duration-200 hover:opacity-80"
style={{
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
OCR Settings
</button>
</div>
<div className="flex-1">
<CameraScanner
onCardScanned={handleCardScanned}
onError={handleError}
/>
</div>
</div>
</div>
{/* Scanned Cards Queue */}
<div className="lg:col-span-2 flex flex-col">
<div className="flex-1 rounded-xl p-6 flex flex-col" style={{ backgroundColor: 'var(--bg-secondary)', border: '1px solid var(--border)' }}>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
Scanned Cards
</h2>
<div className="flex items-center gap-2">
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{scannedCards.length} cards
</div>
{scannedCards.length > 0 && (
<button
onClick={clearScannedCards}
className="px-3 py-1 rounded-lg text-sm border hover:opacity-80"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Clear All
</button>
)}
</div>
</div>
{/* Scanned Cards Queue - Scrollable */}
<div className="flex-1 overflow-y-auto">
<div className="space-y-3">
{scannedCards.length === 0 ? (
<div className="text-center py-8" style={{ color: 'var(--text-secondary)' }}>
<div className="text-4xl mb-2">📱</div>
<div className="font-medium">No cards scanned yet</div>
<div className="text-sm">Start scanning to see cards here</div>
</div>
) : (
scannedCards.map((card) => (
<div
key={card.id}
className={`flex gap-4 p-4 rounded-lg border ${card.processed ? 'opacity-60' : ''}`}
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: selectedCards.has(card.id) ? 'var(--accent-ember)' : 'var(--border)',
borderWidth: selectedCards.has(card.id) ? '2px' : '1px'
}}
>
{/* Card Thumbnail with Checkbox Overlay */}
<div className="relative flex-shrink-0">
<div className="w-20 h-28 rounded-lg overflow-hidden bg-gray-200 flex items-center justify-center">
{card.image_url ? (
<img
src={card.image_url}
alt={card.name}
className="w-full h-full object-cover"
/>
) : (
<div className="text-center text-xs text-gray-500 p-2">
<div className="text-2xl mb-1">🃏</div>
<div>No Image</div>
</div>
)}
</div>
{/* Checkbox Overlay */}
{!card.processed && (
<div className="absolute top-1 left-1">
<input
type="checkbox"
checked={selectedCards.has(card.id)}
onChange={() => toggleCardSelection(card.id)}
className="w-5 h-5 rounded border-2 border-white shadow-lg"
style={{ accentColor: 'var(--accent-ember)' }}
/>
</div>
)}
</div>
{/* Card Content */}
<div className="flex-1 min-w-0">
{/* Confidence Badge */}
{card.confidence && (
<div className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mb-2"
style={{
backgroundColor: card.confidence >= 90 ? 'var(--accent-gold)' :
card.confidence >= 70 ? 'var(--accent-ember)' : 'var(--text-secondary)',
color: 'white'
}}>
{Math.round(card.confidence)}% confidence
</div>
)}
{/* Title and Quantity Row */}
<div className="flex items-start justify-between mb-2 gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg truncate" style={{ color: 'var(--text-primary)' }}>
{card.name}
</h3>
{/* Database Status */}
{card.isExisting && (
<div className="text-xs text-green-600 font-medium">
Found in database
</div>
)}
</div>
{/* Quantity Controls */}
{!card.processed && (
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={() => decrementCardQuantity(card.id)}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold hover:opacity-80"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
>
</button>
<span className="min-w-[2rem] text-center font-medium" style={{ color: 'var(--text-primary)' }}>
{card.quantity || 1}
</span>
<button
onClick={() => incrementCardQuantity(card.id)}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold hover:opacity-80"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
>
+
</button>
</div>
)}
</div>
{/* Card Details */}
<div className="space-y-1 mb-3">
{card.set && (
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Set:</span> {card.set}
{card.setCode && <span className="ml-2 text-xs">({card.setCode})</span>}
</div>
)}
{card.cardNumber && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Number:</span> {card.cardNumber}
</div>
)}
{card.cardType && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Type:</span> {card.cardType}
</div>
)}
{card.rarity && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Rarity:</span> {card.rarity}
</div>
)}
{card.hp && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">HP:</span> {card.hp}
</div>
)}
{card.manaCost && (
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<span className="font-semibold">Mana Cost:</span> {card.manaCost}
</div>
)}
{card.ocrText && (
<div className="mt-2">
<div className="text-xs font-semibold mb-1" style={{ color: 'var(--text-secondary)' }}>
Scanned Text:
</div>
<div className="text-xs p-2 rounded max-h-16 overflow-y-auto"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-secondary)' }}>
{card.ocrText.substring(0, 150)}{card.ocrText.length > 150 ? '...' : ''}
</div>
</div>
)}
</div>
{/* Actions */}
{!card.processed ? (
<div className="space-y-2">
{/* Primary Actions Row */}
<div className="flex gap-2">
<button
onClick={() => addSingleCardToOwned(card)}
className="flex-1 px-3 py-2 rounded text-sm font-medium hover:opacity-80 flex items-center justify-center gap-1"
style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }}
>
💎 Mark Owned
</button>
<button
onClick={() => removeScannedCard(card.id)}
className="px-3 py-2 rounded text-sm hover:opacity-80"
style={{ color: 'var(--text-secondary)', backgroundColor: 'var(--bg-secondary)' }}
title="Remove"
>
🗑
</button>
</div>
{/* Secondary Actions Row */}
<div className="flex gap-2">
{collections.length > 0 && (
<select
onChange={(e) => e.target.value && addSingleCardToCollection(card, e.target.value)}
className="flex-1 px-3 py-2 rounded text-sm"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
>
<option value="">📚 Add to Collection</option>
{collections.map(collection => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
)}
{decks.length > 0 && (
<select
onChange={(e) => e.target.value && addSingleCardToDeck(card, e.target.value)}
className="flex-1 px-3 py-2 rounded text-sm"
style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)' }}
>
<option value="">🃏 Add to Deck</option>
{decks.map(deck => (
<option key={deck.id} value={deck.id}>
{deck.name}
</option>
))}
</select>
)}
</div>
</div>
) : (
<div className="text-sm flex items-center gap-2" style={{ color: 'var(--accent-ember)' }}>
<span></span>
<span>Added to {card.processedAction}</span>
</div>
)}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
{/* Floating Bulk Actions Toolbar */}
{selectedCards.size > 0 && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-50">
<div className="rounded-2xl shadow-2xl border px-6 py-4 flex items-center gap-4 max-w-4xl"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
backdropFilter: 'blur(10px)'
}}>
{/* Selection Count */}
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold text-white"
style={{ backgroundColor: 'var(--accent-ember)' }}>
{selectedCards.size}
</div>
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
{selectedCards.size === 1 ? 'card selected' : 'cards selected'}
</span>
</div>
{/* Divider */}
<div className="w-px h-8" style={{ backgroundColor: 'var(--border)' }}></div>
{/* Quick Actions */}
<div className="flex items-center gap-3">
<button
onClick={() => {
setBulkAction('owned');
handleBulkAction();
}}
disabled={isProcessing}
className="px-4 py-2 rounded-lg font-medium hover:opacity-80 disabled:opacity-50 flex items-center gap-2"
style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }}
>
💎 Mark Owned
</button>
{collections.length > 0 && (
<select
onChange={(e) => {
if (e.target.value) {
setBulkAction('collection');
setBulkTarget(e.target.value);
setTimeout(() => handleBulkAction(), 100);
}
}}
disabled={isProcessing}
className="px-4 py-2 rounded-lg font-medium"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white', border: 'none' }}
>
<option value="">📚 Add to Collection</option>
{collections.map(collection => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
)}
{decks.length > 0 && (
<select
onChange={(e) => {
if (e.target.value) {
setBulkAction('deck');
setBulkTarget(e.target.value);
setTimeout(() => handleBulkAction(), 100);
}
}}
disabled={isProcessing}
className="px-4 py-2 rounded-lg font-medium"
style={{ backgroundColor: 'var(--accent-flame)', color: 'white', border: 'none' }}
>
<option value="">🃏 Add to Deck</option>
{decks.map(deck => (
<option key={deck.id} value={deck.id}>
{deck.name} ({deck.game})
</option>
))}
</select>
)}
</div>
{/* Divider */}
<div className="w-px h-8" style={{ backgroundColor: 'var(--border)' }}></div>
{/* Clear Selection */}
<button
onClick={() => setSelectedCards(new Set())}
className="px-3 py-2 rounded-lg hover:opacity-80"
style={{ color: 'var(--text-secondary)' }}
title="Clear Selection"
>
</button>
</div>
</div>
)}
{/* Bulk Actions Modal */}
{/* This modal is no longer needed as bulk actions are in a floating toolbar */}
{/* Create Collection Modal */}
{showCreateCollection && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="rounded-xl p-6 max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Create New Collection
</h3>
<input
type="text"
placeholder="Collection name..."
value={newCollectionName}
onChange={(e) => setNewCollectionName(e.target.value)}
className="w-full px-4 py-2 rounded-lg border mb-4"
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
createCollection();
}
}}
/>
<div className="flex gap-3">
<button
onClick={createCollection}
disabled={!newCollectionName.trim()}
className="flex-1 px-4 py-2 rounded-lg font-medium disabled:opacity-50"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
>
Create
</button>
<button
onClick={() => setShowCreateCollection(false)}
className="flex-1 px-4 py-2 rounded-lg border font-medium"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* OCR Settings Modal */}
{showOCRSettings && (
<OCRSettings onClose={() => setShowOCRSettings(false)} />
)}
</div>
</Layout>
);
}