diff --git a/pages/api/cards/search.js b/pages/api/cards/search.js index 6a15dee..4767c09 100644 --- a/pages/api/cards/search.js +++ b/pages/api/cards/search.js @@ -1,212 +1,48 @@ import { sql } from '@vercel/postgres'; export default async function handler(req, res) { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + if (req.method !== 'GET') { return res.status(405).json({ error: 'Method not allowed' }); } try { - const { - query = '', - game = '', - rarity = '', - set = '', - minPrice = '', - maxPrice = '', - page = 1, - limit = 50 - } = req.query; + const { q = '', limit = 20 } = req.query; let result; - let countResult; - - // Build query based on filters - if (query && query.trim() && game && game !== 'all' && rarity && rarity !== 'all' && set && set !== 'all') { - // All filters applied + if (q.trim()) { + // Search by name result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at + SELECT id, name, set_name, rarity, card_type, image_url, market_price, game FROM cards - WHERE (name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`}) - AND game = ${game} - AND rarity = ${rarity} - AND set_name = ${set} - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards - WHERE (name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`}) - AND game = ${game} - AND rarity = ${rarity} - AND set_name = ${set} - `; - } else if (query && query.trim() && game && game !== 'all' && rarity && rarity !== 'all') { - // Query, game, and rarity filters - result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at - FROM cards - WHERE (name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`}) - AND game = ${game} - AND rarity = ${rarity} - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards - WHERE (name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`}) - AND game = ${game} - AND rarity = ${rarity} - `; - } else if (query && query.trim() && game && game !== 'all') { - // Query and game filters - result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at - FROM cards - WHERE (name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`}) - AND game = ${game} - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards - WHERE (name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`}) - AND game = ${game} - `; - } else if (query && query.trim()) { - // Only query filter - result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at - FROM cards - WHERE name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`} - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards - WHERE name ILIKE ${`%${query.trim()}%`} OR oracle_text ILIKE ${`%${query.trim()}%`} - `; - } else if (game && game !== 'all') { - // Only game filter - result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at - FROM cards - WHERE game = ${game} - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards - WHERE game = ${game} - `; - } else if (rarity && rarity !== 'all') { - // Only rarity filter - result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at - FROM cards - WHERE rarity = ${rarity} - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards - WHERE rarity = ${rarity} - `; - } else if (set && set !== 'all') { - // Only set filter - result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at - FROM cards - WHERE set_name = ${set} - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards - WHERE set_name = ${set} + WHERE name ILIKE ${`%${q}%`} + ORDER BY name + LIMIT ${parseInt(limit)} `; } else { - // No filters - get all cards + // Return all cards if no search query result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at + SELECT id, name, set_name, rarity, card_type, image_url, market_price, game FROM cards - ORDER BY name ASC - LIMIT ${parseInt(limit)} OFFSET ${(parseInt(page) - 1) * parseInt(limit)} - `; - countResult = await sql` - SELECT COUNT(*) as total FROM cards + ORDER BY name + LIMIT ${parseInt(limit)} `; } - const total = parseInt(countResult.rows[0]?.total || 0); - - // Get unique values for filters - const [gamesResult, raritiesResult, setsResult] = await Promise.all([ - sql`SELECT DISTINCT game FROM cards WHERE game IS NOT NULL ORDER BY game`, - sql`SELECT DISTINCT rarity FROM cards WHERE rarity IS NOT NULL ORDER BY rarity`, - sql`SELECT DISTINCT set_name FROM cards WHERE set_name IS NOT NULL ORDER BY set_name` - ]); - - res.status(200).json({ - success: true, - cards: result.rows || [], - pagination: { - page: parseInt(page), - limit: parseInt(limit), - total, - pages: Math.ceil(total / limit) - }, - filters: { - games: Array.isArray(gamesResult.rows) ? gamesResult.rows.map(row => row.game) : [], - rarities: Array.isArray(raritiesResult.rows) ? raritiesResult.rows.map(row => row.rarity) : [], - sets: Array.isArray(setsResult.rows) ? setsResult.rows.map(row => row.set_name) : [] - } - }); + res.status(200).json(result.rows); } catch (error) { - console.error('Card search error:', error); - res.status(500).json({ - error: 'Search failed', - details: error.message - }); + console.error('Error searching cards:', error); + res.status(500).json({ error: 'Internal server error' }); } -} \ No newline at end of file +} diff --git a/pages/api/collections/[id].js b/pages/api/collections/[id].js index 6a538d3..f4e7397 100644 --- a/pages/api/collections/[id].js +++ b/pages/api/collections/[id].js @@ -40,7 +40,7 @@ async function handler(req, res) { cards.name, cards.set_name, cards.rarity, - cards.type, + cards.card_type, cards.image_url, cards.market_price FROM collection_cards cc diff --git a/pages/collection/[id].js b/pages/collection/[id].js index 27550d8..7dd3e15 100644 --- a/pages/collection/[id].js +++ b/pages/collection/[id].js @@ -7,10 +7,10 @@ export default function CollectionView() { const router = useRouter(); const { id } = router.query; - // Mock user data for now + // Get user from auth context - for now using admin user const user = { - email: 'me@randallstillwell.com', - role: 'user' + email: 'admin@tcgvault.com', + role: 'admin' }; const [collection, setCollection] = useState(null); @@ -26,91 +26,11 @@ export default function CollectionView() { const [selectedType, setSelectedType] = useState('all'); const [sortBy, setSortBy] = useState('name'); const [viewMode, setViewMode] = useState('grid'); // grid or list + const [searchCards, setSearchCards] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [showSearchResults, setShowSearchResults] = useState(false); - // Mock collection data - const mockCollection = { - id: 1, - name: "King PikaRomulus", - description: "A competitive Pokemon deck focused on Pikachu and powerful electric types", - creator: "Emberwing", - format: "Standard", - cost: "$1,430", - cardCount: 60, - createdAt: "2023-04-12", - lastUpdated: "2 months ago", - isPublic: true, - isOfficial: false, - tags: ["competitive", "electric", "pikachu", "standard"], - tcg: "Pokemon", - playGuide: "How to play King PikaRomulus", - views: 2847, - favorites: 156, - copies: 89 - }; - // Mock cards data - const mockCards = [ - { - id: 1, - name: "Pikachu VMAX", - set: "Vivid Voltage", - rarity: "Rainbow Rare", - type: "Electric", - cost: 45.99, - quantity: 1, - image: "https://images.pokemontcg.io/swsh4/188_hires.png" - }, - { - id: 2, - name: "Professor's Research", - set: "Champion's Path", - rarity: "Uncommon", - type: "Trainer", - cost: 2.50, - quantity: 4, - image: "https://images.pokemontcg.io/swsh35/62_hires.png" - }, - { - id: 3, - name: "Quick Ball", - set: "Sword & Shield", - rarity: "Uncommon", - type: "Trainer", - cost: 1.25, - quantity: 4, - image: "https://images.pokemontcg.io/swsh1/179_hires.png" - }, - { - id: 4, - name: "Lightning Energy", - set: "Basic Energy", - rarity: "Common", - type: "Energy", - cost: 0.10, - quantity: 12, - image: "https://images.pokemontcg.io/base1/100_hires.png" - }, - { - id: 5, - name: "Zapdos V", - set: "Chilling Reign", - rarity: "Ultra Rare", - type: "Electric", - cost: 8.75, - quantity: 2, - image: "https://images.pokemontcg.io/swsh6/166_hires.png" - }, - { - id: 6, - name: "Ultra Ball", - set: "Plasma Freeze", - rarity: "Uncommon", - type: "Trainer", - cost: 3.20, - quantity: 3, - image: "https://images.pokemontcg.io/pl9/122_hires.png" - } - ]; useEffect(() => { if (id) { @@ -127,15 +47,13 @@ export default function CollectionView() { setCards(data.cards || []); } else { console.error('Failed to fetch collection'); - // Fallback to mock data for now - setCollection(mockCollection); - setCards(mockCards); + setCollection(null); + setCards([]); } } catch (error) { console.error('Error fetching collection:', error); - // Fallback to mock data for now - setCollection(mockCollection); - setCards(mockCards); + setCollection(null); + setCards([]); } finally { setLoading(false); } @@ -161,10 +79,58 @@ export default function CollectionView() { console.log('Saving copy of collection'); }; + const handleSearchCards = async (query) => { + if (!query.trim()) { + setSearchResults([]); + setShowSearchResults(false); + return; + } + + try { + const response = await fetch(`/api/cards/search?q=${encodeURIComponent(query)}&limit=10`); + if (response.ok) { + const results = await response.json(); + setSearchResults(results); + setShowSearchResults(true); + } + } catch (error) { + console.error('Error searching cards:', error); + } + }; + + const handleAddCard = async (card, quantity = 1) => { + try { + const response = await fetch(`/api/collections/${id}/cards`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + cardId: card.id, + quantity + }) + }); + + if (response.ok) { + // Refresh collection data + fetchCollectionData(); + setSearchCards(''); + setSearchResults([]); + setShowSearchResults(false); + } else { + const error = await response.json(); + alert(error.error || 'Failed to add card'); + } + } catch (error) { + console.error('Error adding card:', error); + alert('Network error. Please try again.'); + } + }; + const filteredCards = cards.filter(card => { const matchesSearch = card.name.toLowerCase().includes(searchQuery.toLowerCase()); const matchesRarity = selectedRarity === 'all' || card.rarity === selectedRarity; - const matchesType = selectedType === 'all' || card.type === selectedType; + const matchesType = selectedType === 'all' || card.card_type === selectedType; return matchesSearch && matchesRarity && matchesType; }); @@ -173,17 +139,17 @@ export default function CollectionView() { case 'name': return a.name.localeCompare(b.name); case 'cost': - return b.cost - a.cost; + return b.market_price - a.market_price; case 'rarity': return a.rarity.localeCompare(b.rarity); case 'type': - return a.type.localeCompare(b.type); + return a.card_type.localeCompare(b.card_type); default: return 0; } }); - const totalValue = cards.reduce((sum, card) => sum + (card.cost * card.quantity), 0); + const totalValue = cards.reduce((sum, card) => sum + (card.market_price * card.quantity), 0); const totalCards = cards.reduce((sum, card) => sum + card.quantity, 0); if (loading) { @@ -222,7 +188,9 @@ export default function CollectionView() {
-
šŸ‘‘
+
šŸƒ

@@ -241,25 +209,30 @@ export default function CollectionView() {
šŸ‘¤
- Crafted by {collection.creator} + Created by {collection.creator_email}

• - Format: {collection.format} + TCG: {collection.tcg} • - Cost: {collection.cost} + Value: ${totalValue.toFixed(2)} • - Appears in: {collection.playGuide} - - +4 more - + Cards: {totalCards}
- Created {collection.createdAt} + Created {new Date(collection.created_at).toLocaleDateString()} • - Last updated {collection.lastUpdated} + Last updated {new Date(collection.updated_at).toLocaleDateString()} + {collection.is_public && ( + <> + • + + šŸŒ Public + + + )}
@@ -340,15 +313,21 @@ export default function CollectionView() {
Total Value
-
{collection.views}
-
Views
+
{collection.tcg}
+
Game
-
{collection.favorites}
-
Favorites
+
{collection.is_public ? 'Public' : 'Private'}
+
Visibility
+ {/* Collaboration Manager */} + + {/* Filters */}
@@ -467,7 +446,7 @@ export default function CollectionView() { >
{card.name} { @@ -484,7 +463,7 @@ export default function CollectionView() { {card.name}
- ${card.cost} + ${card.market_price}
@@ -504,7 +483,7 @@ export default function CollectionView() { onClick={() => router.push(`/card/${card.id}`)} > {card.name} { @@ -516,17 +495,17 @@ export default function CollectionView() { {card.name}
- {card.set} • {card.rarity} • {card.type} + {card.set_name} • {card.rarity} • {card.card_type}
-
-
- ${card.cost} +
+
+ ${card.market_price} +
+
+ Qty: {card.quantity} +
-
- Qty: {card.quantity} -
-
))} @@ -558,7 +537,7 @@ export default function CollectionView() { {/* Quick add section */} -
+

Quick Add

@@ -566,6 +545,11 @@ export default function CollectionView() { { + setSearchCards(e.target.value); + handleSearchCards(e.target.value); + }} className="w-full px-4 py-3 rounded-lg border transition-all duration-200" style={{ backgroundColor: 'var(--bg-primary)', @@ -573,15 +557,36 @@ export default function CollectionView() { color: 'var(--text-primary)' }} /> - + + {/* Search Results */} + {showSearchResults && searchResults.length > 0 && ( +
+ {searchResults.map((card) => ( +
handleAddCard(card)} + > + {card.name} { + e.target.src = 'https://via.placeholder.com/40x56/6366f1/ffffff?text=?'; + }} + /> +
+
{card.name}
+
{card.set_name} • {card.rarity}
+
+
+
${card.market_price}
+
{card.game}
+
+
+ ))} +
+ )}
diff --git a/scripts/create-sample-cards.js b/scripts/create-sample-cards.js new file mode 100755 index 0000000..13d51e9 --- /dev/null +++ b/scripts/create-sample-cards.js @@ -0,0 +1,125 @@ +#!/usr/bin/env node + +import { config } from 'dotenv'; +import { sql } from '@vercel/postgres'; + +// Load environment variables +config({ path: '.env.local' }); + +async function createSampleCards() { + try { + console.log('šŸƒ Creating sample cards...\n'); + + const sampleCards = [ + { + name: 'Lightning Bolt', + set_name: 'Alpha', + set_code: 'LEA', + card_number: '161', + rarity: 'Common', + game: 'MTG', + mana_cost: '{R}', + cmc: 1, + card_type: 'Instant', + colors: '["Red"]', + oracle_text: 'Lightning Bolt deals 3 damage to any target.', + image_url: 'https://cards.scryfall.io/normal/front/c/e/ce711943-c1a1-43a0-8b89-8d169cfb8e06.jpg', + market_price: 2.50 + }, + { + name: 'Black Lotus', + set_name: 'Alpha', + set_code: 'LEA', + card_number: '232', + rarity: 'Rare', + game: 'MTG', + mana_cost: '{0}', + cmc: 0, + card_type: 'Artifact', + colors: '[]', + oracle_text: '{T}, Sacrifice Black Lotus: Add three mana of any one color.', + image_url: 'https://cards.scryfall.io/normal/front/b/d/bd8fa327-dd41-4737-8f19-2cf5eb1f7cdd.jpg', + market_price: 25000.00 + }, + { + name: 'Pikachu', + set_name: 'Base Set', + set_code: 'BS1', + card_number: '25', + rarity: 'Common', + game: 'Pokemon', + card_type: 'Basic Pokemon', + oracle_text: 'When several of these Pokemon gather, their electricity could build and cause lightning storms.', + image_url: 'https://images.pokemontcg.io/base1/25_hires.png', + market_price: 8.50 + }, + { + name: 'Charizard', + set_name: 'Base Set', + set_code: 'BS1', + card_number: '4', + rarity: 'Rare Holo', + game: 'Pokemon', + card_type: 'Stage 2 Pokemon', + oracle_text: 'Spits fire that is hot enough to melt boulders. Known to cause forest fires unintentionally.', + image_url: 'https://images.pokemontcg.io/base1/4_hires.png', + market_price: 350.00 + }, + { + name: 'Mickey Mouse - Brave Little Tailor', + set_name: 'The First Chapter', + set_code: 'TFC', + card_number: '1', + rarity: 'Legendary', + game: 'Lorcana', + card_type: 'Character', + oracle_text: 'Bravery - When you play this character, you may banish chosen character.', + image_url: 'https://cdn.lorcana-api.com/images/tfc/001_en_mickey_mouse-716.webp', + market_price: 45.00 + }, + { + name: 'Elsa - Snow Queen', + set_name: 'The First Chapter', + set_code: 'TFC', + card_number: '43', + rarity: 'Super Rare', + game: 'Lorcana', + card_type: 'Character', + oracle_text: 'Deep Freeze - Exert chosen character. They can\'t ready at the start of their next turn.', + image_url: 'https://cdn.lorcana-api.com/images/tfc/043_en_elsa-716.webp', + market_price: 15.75 + } + ]; + + for (const card of sampleCards) { + await sql` + INSERT INTO cards ( + name, set_name, set_code, card_number, rarity, game, + mana_cost, cmc, card_type, colors, oracle_text, + image_url, market_price, verified + ) VALUES ( + ${card.name}, ${card.set_name}, ${card.set_code}, ${card.card_number}, + ${card.rarity}, ${card.game}, ${card.mana_cost || null}, ${card.cmc || null}, + ${card.card_type}, ${card.colors || null}, ${card.oracle_text}, + ${card.image_url}, ${card.market_price}, true + ) + `; + console.log(`āœ… Added ${card.name} (${card.game})`); + } + + console.log('\nšŸŽ‰ Sample cards created successfully!'); + console.log('\nšŸƒ Available Cards:'); + console.log(' • Lightning Bolt (MTG) - $2.50'); + console.log(' • Black Lotus (MTG) - $25,000'); + console.log(' • Pikachu (Pokemon) - $8.50'); + console.log(' • Charizard (Pokemon) - $350'); + console.log(' • Mickey Mouse (Lorcana) - $45'); + console.log(' • Elsa (Lorcana) - $15.75'); + + } catch (error) { + console.error('āŒ Failed to create sample cards:', error.message); + process.exit(1); + } +} + +createSampleCards(); diff --git a/scripts/create-test-users.js b/scripts/create-test-users.js new file mode 100755 index 0000000..0b9ee70 --- /dev/null +++ b/scripts/create-test-users.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +import { config } from 'dotenv'; +import { sql } from '@vercel/postgres'; +import bcrypt from 'bcryptjs'; + +// Load environment variables +config({ path: '.env.local' }); + +async function createTestUsers() { + try { + console.log('�� Creating test users...\n'); + + // Create Alice (collaborator) + const alicePassword = await bcrypt.hash('alice123', 12); + await sql` + INSERT INTO users (email, password, role) + VALUES ('alice@tcgvault.com', ${alicePassword}, 'user') + ON CONFLICT (email) DO NOTHING + `; + console.log('āœ… Created Alice (alice@tcgvault.com / alice123)'); + + // Create Bob (collaborator) + const bobPassword = await bcrypt.hash('bob123', 12); + await sql` + INSERT INTO users (email, password, role) + VALUES ('bob@tcgvault.com', ${bobPassword}, 'user') + ON CONFLICT (email) DO NOTHING + `; + console.log('āœ… Created Bob (bob@tcgvault.com / bob123)'); + + console.log('\nšŸŽ‰ Test users created successfully!'); + console.log('\nšŸ‘„ Available Test Accounts:'); + console.log(' 1. admin@tcgvault.com / admin123 (Admin)'); + console.log(' 2. alice@tcgvault.com / alice123 (User)'); + console.log(' 3. bob@tcgvault.com / bob123 (User)'); + + } catch (error) { + console.error('āŒ Failed to create test users:', error.message); + process.exit(1); + } +} + +createTestUsers();