Implement complete card scanning and collection flow - ready for local testing
This commit is contained in:
parent
08e2664a9a
commit
e9b2da09a5
4 changed files with 702 additions and 9 deletions
169
api/cards/find-or-create.js
Normal file
169
api/cards/find-or-create.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
const { Pool } = require('pg');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
// Middleware to verify user authentication
|
||||
function verifyAuth(req) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('No token provided');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, jwtSecret);
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Verify authentication
|
||||
const user = verifyAuth(req);
|
||||
|
||||
const {
|
||||
name,
|
||||
game,
|
||||
setName = null,
|
||||
setCode = null,
|
||||
rarity = null,
|
||||
cardType = null,
|
||||
manaCost = null,
|
||||
ocrConfidence = null,
|
||||
ocrRawText = null,
|
||||
imageUrl = null
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !game) {
|
||||
return res.status(400).json({
|
||||
error: 'Card name and game are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Try to find existing card first
|
||||
let searchQuery = `
|
||||
SELECT id, name, set_name, set_code, rarity, game, card_type, mana_cost,
|
||||
current_price, stock_image_url, image_url, verified
|
||||
FROM cards
|
||||
WHERE LOWER(name) = LOWER($1) AND UPPER(game) = UPPER($2)
|
||||
`;
|
||||
|
||||
let searchParams = [name.trim(), game.trim()];
|
||||
|
||||
// If set name is provided, try to match more specifically
|
||||
if (setName) {
|
||||
searchQuery += ` AND (set_name IS NULL OR LOWER(set_name) = LOWER($3))`;
|
||||
searchParams.push(setName.trim());
|
||||
}
|
||||
|
||||
searchQuery += ` ORDER BY verified DESC, created_at DESC LIMIT 1`;
|
||||
|
||||
const existingCard = await client.query(searchQuery, searchParams);
|
||||
|
||||
if (existingCard.rows.length > 0) {
|
||||
// Card found, return it
|
||||
const card = existingCard.rows[0];
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
found: true,
|
||||
message: 'Card found in database',
|
||||
card: {
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
setName: card.set_name,
|
||||
setCode: card.set_code,
|
||||
rarity: card.rarity,
|
||||
game: card.game,
|
||||
cardType: card.card_type,
|
||||
manaCost: card.mana_cost,
|
||||
currentPrice: card.current_price,
|
||||
stockImageUrl: card.stock_image_url,
|
||||
imageUrl: card.image_url,
|
||||
verified: card.verified
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Card not found, create new one
|
||||
const insertQuery = `
|
||||
INSERT INTO cards (
|
||||
name, game, set_name, set_code, rarity, card_type, mana_cost,
|
||||
ocr_confidence, ocr_raw_text, image_url, verified, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
)
|
||||
RETURNING id, name, set_name, set_code, rarity, game, card_type, mana_cost,
|
||||
ocr_confidence, ocr_raw_text, image_url, verified, created_at
|
||||
`;
|
||||
|
||||
const insertParams = [
|
||||
name.trim(),
|
||||
game.trim().toUpperCase(),
|
||||
setName?.trim() || null,
|
||||
setCode?.trim() || null,
|
||||
rarity?.trim() || null,
|
||||
cardType?.trim() || null,
|
||||
manaCost?.trim() || null,
|
||||
ocrConfidence || null,
|
||||
ocrRawText?.trim() || null,
|
||||
imageUrl?.trim() || null,
|
||||
false // New cards from OCR are unverified by default
|
||||
];
|
||||
|
||||
const newCard = await client.query(insertQuery, insertParams);
|
||||
const card = newCard.rows[0];
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
found: false,
|
||||
message: 'New card created from scan data',
|
||||
card: {
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
setName: card.set_name,
|
||||
setCode: card.set_code,
|
||||
rarity: card.rarity,
|
||||
game: card.game,
|
||||
cardType: card.card_type,
|
||||
manaCost: card.mana_cost,
|
||||
currentPrice: null,
|
||||
stockImageUrl: null,
|
||||
imageUrl: card.image_url,
|
||||
verified: card.verified,
|
||||
ocrConfidence: card.ocr_confidence,
|
||||
ocrRawText: card.ocr_raw_text,
|
||||
createdAt: card.created_at
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Find or create card error:', error);
|
||||
|
||||
if (error.message === 'No token provided' || error.message === 'Invalid token') {
|
||||
res.status(401).json({ error: error.message });
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
174
api/collections/add-card.js
Normal file
174
api/collections/add-card.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
const { Pool } = require('pg');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
// Middleware to verify user authentication
|
||||
function verifyAuth(req) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('No token provided');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, jwtSecret);
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Verify authentication
|
||||
const user = verifyAuth(req);
|
||||
|
||||
const {
|
||||
collectionId,
|
||||
cardId,
|
||||
quantity = 1,
|
||||
condition = 'near-mint',
|
||||
notes = null,
|
||||
purchasePrice = null
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!collectionId || !cardId) {
|
||||
return res.status(400).json({
|
||||
error: 'Collection ID and Card ID are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify collection ownership
|
||||
const collectionCheck = await client.query(
|
||||
'SELECT id, name FROM user_collections WHERE id = $1 AND user_id = $2',
|
||||
[collectionId, user.userId]
|
||||
);
|
||||
|
||||
if (collectionCheck.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: 'Collection not found or access denied'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify card exists
|
||||
const cardCheck = await client.query(
|
||||
'SELECT id, name, game, rarity, current_price FROM cards WHERE id = $1',
|
||||
[cardId]
|
||||
);
|
||||
|
||||
if (cardCheck.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: 'Card not found'
|
||||
});
|
||||
}
|
||||
|
||||
const card = cardCheck.rows[0];
|
||||
const collection = collectionCheck.rows[0];
|
||||
|
||||
// Check if card already exists in collection with same condition
|
||||
const existingCard = await client.query(`
|
||||
SELECT id, quantity
|
||||
FROM collection_cards
|
||||
WHERE collection_id = $1 AND card_id = $2 AND condition = $3
|
||||
`, [collectionId, cardId, condition]);
|
||||
|
||||
let result;
|
||||
|
||||
if (existingCard.rows.length > 0) {
|
||||
// Update existing entry - increase quantity
|
||||
const newQuantity = existingCard.rows[0].quantity + quantity;
|
||||
|
||||
await client.query(`
|
||||
UPDATE collection_cards
|
||||
SET quantity = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2
|
||||
`, [newQuantity, existingCard.rows[0].id]);
|
||||
|
||||
result = {
|
||||
action: 'updated',
|
||||
previousQuantity: existingCard.rows[0].quantity,
|
||||
newQuantity: newQuantity
|
||||
};
|
||||
} else {
|
||||
// Create new collection card entry
|
||||
const insertResult = await client.query(`
|
||||
INSERT INTO collection_cards (
|
||||
collection_id, card_id, quantity, condition, notes, purchase_price
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, added_at
|
||||
`, [collectionId, cardId, quantity, condition, notes, purchasePrice]);
|
||||
|
||||
result = {
|
||||
action: 'added',
|
||||
collectionCardId: insertResult.rows[0].id,
|
||||
addedAt: insertResult.rows[0].added_at
|
||||
};
|
||||
}
|
||||
|
||||
// Get updated collection stats
|
||||
const statsQuery = await client.query(`
|
||||
SELECT
|
||||
COUNT(cc.id) as total_entries,
|
||||
COALESCE(SUM(cc.quantity), 0) as total_cards,
|
||||
COALESCE(SUM(cc.quantity * COALESCE(c.current_price, 0)), 0) as total_value
|
||||
FROM collection_cards cc
|
||||
LEFT JOIN cards c ON cc.card_id = c.id
|
||||
WHERE cc.collection_id = $1
|
||||
`, [collectionId]);
|
||||
|
||||
const stats = statsQuery.rows[0];
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `Card ${result.action} successfully`,
|
||||
result: {
|
||||
...result,
|
||||
card: {
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
game: card.game,
|
||||
rarity: card.rarity,
|
||||
currentPrice: card.current_price
|
||||
},
|
||||
collection: {
|
||||
id: collection.id,
|
||||
name: collection.name
|
||||
},
|
||||
quantity: quantity,
|
||||
condition: condition,
|
||||
collectionStats: {
|
||||
totalEntries: parseInt(stats.total_entries),
|
||||
totalCards: parseInt(stats.total_cards),
|
||||
totalValue: parseFloat(stats.total_value)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Add card to collection error:', error);
|
||||
|
||||
if (error.message === 'No token provided' || error.message === 'Invalid token') {
|
||||
res.status(401).json({ error: error.message });
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
121
api/collections/index.js
Normal file
121
api/collections/index.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
const { Pool } = require('pg');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
// Middleware to verify user authentication
|
||||
function verifyAuth(req) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('No token provided');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, jwtSecret);
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Verify authentication
|
||||
const user = verifyAuth(req);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
// Get user's collections
|
||||
const collectionsQuery = `
|
||||
SELECT
|
||||
uc.id, uc.name, uc.description, uc.is_public, uc.created_at, uc.updated_at,
|
||||
COUNT(cc.id) as total_cards,
|
||||
COALESCE(SUM(cc.quantity * COALESCE(c.current_price, 0)), 0) as total_value
|
||||
FROM user_collections uc
|
||||
LEFT JOIN collection_cards cc ON uc.id = cc.collection_id
|
||||
LEFT JOIN cards c ON cc.card_id = c.id
|
||||
WHERE uc.user_id = $1
|
||||
GROUP BY uc.id, uc.name, uc.description, uc.is_public, uc.created_at, uc.updated_at
|
||||
ORDER BY uc.created_at DESC
|
||||
`;
|
||||
|
||||
const collections = await client.query(collectionsQuery, [user.userId]);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
collections: collections.rows.map(collection => ({
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
description: collection.description,
|
||||
isPublic: collection.is_public,
|
||||
totalCards: parseInt(collection.total_cards) || 0,
|
||||
totalValue: parseFloat(collection.total_value) || 0,
|
||||
createdAt: collection.created_at,
|
||||
updatedAt: collection.updated_at
|
||||
}))
|
||||
});
|
||||
|
||||
} else if (req.method === 'POST') {
|
||||
// Create new collection
|
||||
const { name, description, isPublic = false } = req.body;
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Collection name is required' });
|
||||
}
|
||||
|
||||
const insertQuery = `
|
||||
INSERT INTO user_collections (user_id, name, description, is_public)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, name, description, is_public, created_at, updated_at
|
||||
`;
|
||||
|
||||
const result = await client.query(insertQuery, [
|
||||
user.userId,
|
||||
name.trim(),
|
||||
description?.trim() || null,
|
||||
isPublic
|
||||
]);
|
||||
|
||||
const newCollection = result.rows[0];
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Collection created successfully',
|
||||
collection: {
|
||||
id: newCollection.id,
|
||||
name: newCollection.name,
|
||||
description: newCollection.description,
|
||||
isPublic: newCollection.is_public,
|
||||
totalCards: 0,
|
||||
totalValue: 0,
|
||||
createdAt: newCollection.created_at,
|
||||
updatedAt: newCollection.updated_at
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Collections API error:', error);
|
||||
|
||||
if (error.message === 'No token provided' || error.message === 'Invalid token') {
|
||||
res.status(401).json({ error: error.message });
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import CameraScanner from '../components/CameraScanner';
|
||||
import GlowingCard from '../components/GlowingCard';
|
||||
import CardImageDisplay from '../components/CardImageDisplay';
|
||||
import { cardMatcher } from '../services/cardMatcher';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
interface ScannedCard {
|
||||
id: string;
|
||||
|
|
@ -14,11 +15,91 @@ interface ScannedCard {
|
|||
timestamp: number;
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
totalCards: number;
|
||||
totalValue: number;
|
||||
}
|
||||
|
||||
const Scanner: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [scannedCards, setScannedCards] = useState<ScannedCard[]>([]);
|
||||
const [isProcessingMatch, setIsProcessingMatch] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [selectedCollectionId, setSelectedCollectionId] = useState<string | null>(null);
|
||||
const [isLoadingCollections, setIsLoadingCollections] = useState(false);
|
||||
const [isAddingToCollection, setIsAddingToCollection] = useState(false);
|
||||
|
||||
// Load user's collections on component mount
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadCollections();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadCollections = async () => {
|
||||
setIsLoadingCollections(true);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/collections', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCollections(data.collections || []);
|
||||
|
||||
// Auto-select first collection if available
|
||||
if (data.collections && data.collections.length > 0) {
|
||||
setSelectedCollectionId(data.collections[0].id);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to load collections');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading collections:', error);
|
||||
} finally {
|
||||
setIsLoadingCollections(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewCollection = async (name: string, description?: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/collections', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
description: description?.trim() || null,
|
||||
isPublic: false
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const newCollection = data.collection;
|
||||
setCollections(prev => [newCollection, ...prev]);
|
||||
setSelectedCollectionId(newCollection.id);
|
||||
return newCollection;
|
||||
} else {
|
||||
throw new Error('Failed to create collection');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating collection:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -90,14 +171,92 @@ const Scanner: React.FC = () => {
|
|||
const scannedCard = scannedCards.find(c => c.id === scannedCardId);
|
||||
if (!scannedCard || !scannedCard.selectedMatch) return;
|
||||
|
||||
if (!selectedCollectionId) {
|
||||
setError('Please select a collection first.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAddingToCollection(true);
|
||||
try {
|
||||
// TODO: Implement collection service
|
||||
console.log('Adding to collection:', scannedCard.selectedMatch);
|
||||
// For now, just remove from scan results
|
||||
removeScannedCard(scannedCardId);
|
||||
} catch (err) {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
// First, ensure the card exists in our database
|
||||
let cardId = scannedCard.selectedMatch.id;
|
||||
|
||||
if (!cardId) {
|
||||
// Need to find or create the card
|
||||
const findOrCreateResponse = await fetch('/api/cards/find-or-create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: scannedCard.selectedMatch.name,
|
||||
game: scannedCard.selectedMatch.game || 'MTG',
|
||||
setName: scannedCard.selectedMatch.set_name,
|
||||
setCode: scannedCard.selectedMatch.set_code,
|
||||
rarity: scannedCard.selectedMatch.rarity,
|
||||
cardType: scannedCard.selectedMatch.card_type,
|
||||
manaCost: scannedCard.selectedMatch.mana_cost,
|
||||
ocrConfidence: scannedCard.ocrConfidence,
|
||||
ocrRawText: scannedCard.ocrText
|
||||
})
|
||||
});
|
||||
|
||||
if (findOrCreateResponse.ok) {
|
||||
const cardData = await findOrCreateResponse.json();
|
||||
cardId = cardData.card.id;
|
||||
} else {
|
||||
throw new Error('Failed to find or create card in database');
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the card to the collection
|
||||
const addResponse = await fetch('/api/collections/add-card', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collectionId: selectedCollectionId,
|
||||
cardId: cardId,
|
||||
quantity: 1,
|
||||
condition: 'near-mint',
|
||||
notes: `Added via OCR scan (${Math.round(scannedCard.ocrConfidence)}% confidence)`
|
||||
})
|
||||
});
|
||||
|
||||
if (addResponse.ok) {
|
||||
const result = await addResponse.json();
|
||||
|
||||
// Update collection stats
|
||||
setCollections(prev => prev.map(collection =>
|
||||
collection.id === selectedCollectionId
|
||||
? {
|
||||
...collection,
|
||||
totalCards: result.result.collectionStats.totalCards,
|
||||
totalValue: result.result.collectionStats.totalValue
|
||||
}
|
||||
: collection
|
||||
));
|
||||
|
||||
// Show success message and remove from scan results
|
||||
setError(null);
|
||||
removeScannedCard(scannedCardId);
|
||||
|
||||
// You could show a success toast here instead
|
||||
console.log(`✅ ${result.message}:`, result.result.card.name);
|
||||
} else {
|
||||
const errorData = await addResponse.json();
|
||||
throw new Error(errorData.error || 'Failed to add card to collection');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to add to collection:', err);
|
||||
setError('Failed to add card to collection.');
|
||||
setError(`Failed to add card to collection: ${err.message}`);
|
||||
} finally {
|
||||
setIsAddingToCollection(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -135,6 +294,66 @@ const Scanner: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Collection Selection */}
|
||||
{user && (
|
||||
<div className="mb-6 bg-white rounded-lg shadow border border-gray-200 p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">📚 Collection Selection</h2>
|
||||
|
||||
{isLoadingCollections ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-indigo-600"></div>
|
||||
<span className="text-gray-600">Loading collections...</span>
|
||||
</div>
|
||||
) : collections.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="collection-select" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select collection to add scanned cards to:
|
||||
</label>
|
||||
<select
|
||||
id="collection-select"
|
||||
value={selectedCollectionId || ''}
|
||||
onChange={(e) => setSelectedCollectionId(e.target.value || null)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
<option value="">Select a collection...</option>
|
||||
{collections.map((collection) => (
|
||||
<option key={collection.id} value={collection.id}>
|
||||
{collection.name} ({collection.totalCards} cards - ${collection.totalValue.toFixed(2)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedCollectionId && (
|
||||
<div className="text-sm text-gray-600">
|
||||
✅ Cards will be added to: <strong>{collections.find(c => c.id === selectedCollectionId)?.name}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<div className="text-gray-600 mb-4">
|
||||
You don't have any collections yet. Create one to start adding scanned cards!
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const name = prompt('Enter collection name:');
|
||||
if (name) {
|
||||
createNewCollection(name).catch(err => {
|
||||
setError('Failed to create collection. Please try again.');
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
➕ Create First Collection
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Camera Scanner */}
|
||||
<div className="mb-8 bg-white rounded-lg shadow border border-gray-200 p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">📸 Live Camera Scanner</h2>
|
||||
|
|
@ -286,9 +505,19 @@ const Scanner: React.FC = () => {
|
|||
</div>
|
||||
<button
|
||||
onClick={() => addToCollection(scannedCard.id)}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2"
|
||||
disabled={isAddingToCollection || !selectedCollectionId}
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span>➕</span> Add to Collection
|
||||
{isAddingToCollection ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Adding...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>➕</span> Add to Collection
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue