diff --git a/pages/api/collections.js b/pages/api/collections.js index a179f29..63947ca 100644 --- a/pages/api/collections.js +++ b/pages/api/collections.js @@ -1,23 +1,79 @@ import { sql } from '@vercel/postgres'; export default async function handler(req, res) { - if (req.method !== 'GET') { - return res.status(405).json({ error: 'Method not allowed' }); + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; } - try { - // For now, return mock collections until we implement user authentication - const mockCollections = [ - { id: 1, name: 'My MTG Collection', game: 'MTG' }, - { id: 2, name: 'Pokemon Favorites', game: 'Pokemon' }, - { id: 3, name: 'Lorcana Disney', game: 'Lorcana' }, - { id: 4, name: 'Rare Cards', game: 'MTG' }, - { id: 5, name: 'Holographic Collection', game: 'Pokemon' } - ]; + if (req.method === 'GET') { + try { + // Get all collections with basic stats + const result = await sql` + SELECT + c.*, + u.email as creator_email, + COUNT(cc.card_id) as card_count, + COALESCE(SUM(cards.market_price * cc.quantity), 0) as total_value + FROM collections c + LEFT JOIN users u ON c.user_id = u.id + LEFT JOIN collection_cards cc ON c.id = cc.collection_id + LEFT JOIN cards ON cc.card_id = cards.id + WHERE c.is_public = true OR c.user_id = 1 + GROUP BY c.id, u.email + ORDER BY c.updated_at DESC + `; - res.status(200).json(mockCollections); - } catch (error) { - console.error('Error fetching collections:', error); - res.status(500).json({ error: 'Failed to fetch collections' }); + const collections = result.rows.map(collection => ({ + id: collection.id, + name: collection.name, + description: collection.description, + tcg: collection.tcg || 'MTG', + cardCount: parseInt(collection.card_count) || 0, + value: parseFloat(collection.total_value) || 0, + lastViewed: collection.updated_at, + createdAt: collection.created_at, + isPublic: collection.is_public, + tags: collection.tags ? collection.tags.split(',') : [], + creator: collection.creator_email + })); + + res.status(200).json(collections); + + } catch (error) { + console.error('Error fetching collections:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else if (req.method === 'POST') { + try { + const { name, description, tcg = 'MTG', isPublic = false, tags = [] } = req.body; + + if (!name || !description) { + return res.status(400).json({ error: 'Name and description are required' }); + } + + // For now, use user_id = 1 (should be from auth token in real implementation) + const userId = 1; + + const result = await sql` + INSERT INTO collections (name, description, tcg, is_public, tags, user_id) + VALUES (${name}, ${description}, ${tcg}, ${isPublic}, ${tags.join(',')}, ${userId}) + RETURNING * + `; + + res.status(201).json(result.rows[0]); + + } catch (error) { + console.error('Error creating collection:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else { + res.status(405).json({ error: 'Method not allowed' }); } } \ No newline at end of file diff --git a/pages/api/collections/[id].js b/pages/api/collections/[id].js new file mode 100644 index 0000000..1f01277 --- /dev/null +++ b/pages/api/collections/[id].js @@ -0,0 +1,118 @@ +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, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + const { id } = req.query; + + if (req.method === 'GET') { + try { + // Get collection details + const collectionResult = await sql` + SELECT + c.*, + u.email as creator_email + FROM collections c + LEFT JOIN users u ON c.user_id = u.id + WHERE c.id = ${id} + `; + + if (collectionResult.rows.length === 0) { + return res.status(404).json({ error: 'Collection not found' }); + } + + const collection = collectionResult.rows[0]; + + // Get cards in the collection + const cardsResult = await sql` + SELECT + cc.*, + cards.name, + cards.set_name, + cards.rarity, + cards.type, + cards.image_url, + cards.market_price + FROM collection_cards cc + JOIN cards ON cc.card_id = cards.id + WHERE cc.collection_id = ${id} + ORDER BY cc.created_at ASC + `; + + const cards = cardsResult.rows; + + // Calculate collection stats + const totalCards = cards.reduce((sum, card) => sum + card.quantity, 0); + const totalValue = cards.reduce((sum, card) => sum + (card.market_price * card.quantity), 0); + + res.status(200).json({ + collection: { + ...collection, + totalCards, + totalValue + }, + cards + }); + + } catch (error) { + console.error('Error fetching collection:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else if (req.method === 'PUT') { + // Update collection + try { + const { name, description, isPublic } = req.body; + + const result = await sql` + UPDATE collections + SET + name = ${name}, + description = ${description}, + is_public = ${isPublic}, + updated_at = NOW() + WHERE id = ${id} + RETURNING * + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Collection not found' }); + } + + res.status(200).json(result.rows[0]); + + } catch (error) { + console.error('Error updating collection:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else if (req.method === 'DELETE') { + // Delete collection + try { + // First delete all cards in the collection + await sql`DELETE FROM collection_cards WHERE collection_id = ${id}`; + + // Then delete the collection + const result = await sql`DELETE FROM collections WHERE id = ${id} RETURNING *`; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Collection not found' }); + } + + res.status(200).json({ message: 'Collection deleted successfully' }); + + } catch (error) { + console.error('Error deleting collection:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else { + res.status(405).json({ error: 'Method not allowed' }); + } +} \ No newline at end of file diff --git a/pages/api/collections/[id]/cards.js b/pages/api/collections/[id]/cards.js new file mode 100644 index 0000000..1532b4d --- /dev/null +++ b/pages/api/collections/[id]/cards.js @@ -0,0 +1,131 @@ +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, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + const { id } = req.query; // collection id + + if (req.method === 'POST') { + // Add card to collection + try { + const { cardId, quantity = 1 } = req.body; + + if (!cardId) { + return res.status(400).json({ error: 'Card ID is required' }); + } + + // Check if card already exists in collection + const existingResult = await sql` + SELECT * FROM collection_cards + WHERE collection_id = ${id} AND card_id = ${cardId} + `; + + if (existingResult.rows.length > 0) { + // Update quantity if card already exists + const result = await sql` + UPDATE collection_cards + SET quantity = quantity + ${quantity} + WHERE collection_id = ${id} AND card_id = ${cardId} + RETURNING * + `; + + res.status(200).json({ + message: 'Card quantity updated in collection', + card: result.rows[0] + }); + } else { + // Add new card to collection + const result = await sql` + INSERT INTO collection_cards (collection_id, card_id, quantity) + VALUES (${id}, ${cardId}, ${quantity}) + RETURNING * + `; + + res.status(201).json({ + message: 'Card added to collection', + card: result.rows[0] + }); + } + + } catch (error) { + console.error('Error adding card to collection:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else if (req.method === 'PUT') { + // Update card quantity in collection + try { + const { cardId, quantity } = req.body; + + if (!cardId || quantity === undefined) { + return res.status(400).json({ error: 'Card ID and quantity are required' }); + } + + if (quantity <= 0) { + // Remove card if quantity is 0 or negative + await sql` + DELETE FROM collection_cards + WHERE collection_id = ${id} AND card_id = ${cardId} + `; + + res.status(200).json({ message: 'Card removed from collection' }); + } else { + // Update quantity + const result = await sql` + UPDATE collection_cards + SET quantity = ${quantity} + WHERE collection_id = ${id} AND card_id = ${cardId} + RETURNING * + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Card not found in collection' }); + } + + res.status(200).json({ + message: 'Card quantity updated', + card: result.rows[0] + }); + } + + } catch (error) { + console.error('Error updating card in collection:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else if (req.method === 'DELETE') { + // Remove card from collection + try { + const { cardId } = req.body; + + if (!cardId) { + return res.status(400).json({ error: 'Card ID is required' }); + } + + const result = await sql` + DELETE FROM collection_cards + WHERE collection_id = ${id} AND card_id = ${cardId} + RETURNING * + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Card not found in collection' }); + } + + res.status(200).json({ message: 'Card removed from collection' }); + + } catch (error) { + console.error('Error removing card from collection:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } else { + res.status(405).json({ error: 'Method not allowed' }); + } +} \ No newline at end of file diff --git a/pages/collection/[id].js b/pages/collection/[id].js new file mode 100644 index 0000000..1cf129d --- /dev/null +++ b/pages/collection/[id].js @@ -0,0 +1,579 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import Layout from '../../components/Layout'; + +export default function CollectionView() { + const router = useRouter(); + const { id } = router.query; + + // Mock user data for now + const user = { + email: 'me@randallstillwell.com', + role: 'user' + }; + + const [collection, setCollection] = useState(null); + const [cards, setCards] = useState([]); + const [loading, setLoading] = useState(true); + const [isFavorited, setIsFavorited] = useState(false); + const [showShareModal, setShowShareModal] = useState(false); + const [copySuccess, setCopySuccess] = useState(false); + + // Filter states + const [searchQuery, setSearchQuery] = useState(''); + const [selectedRarity, setSelectedRarity] = useState('all'); + const [selectedType, setSelectedType] = useState('all'); + const [sortBy, setSortBy] = useState('name'); + const [viewMode, setViewMode] = useState('grid'); // grid or list + + // 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) { + // Simulate API call + setTimeout(() => { + setCollection(mockCollection); + setCards(mockCards); + setLoading(false); + }, 500); + } + }, [id]); + + const handleCopyLink = async () => { + try { + await navigator.clipboard.writeText(window.location.href); + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + } catch (err) { + console.error('Failed to copy link:', err); + } + }; + + const handleFavorite = () => { + setIsFavorited(!isFavorited); + // Here you would typically make an API call + }; + + const handleSaveCopy = () => { + // Logic to save a copy to user's collections + console.log('Saving copy of collection'); + }; + + 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; + return matchesSearch && matchesRarity && matchesType; + }); + + const sortedCards = [...filteredCards].sort((a, b) => { + switch (sortBy) { + case 'name': + return a.name.localeCompare(b.name); + case 'cost': + return b.cost - a.cost; + case 'rarity': + return a.rarity.localeCompare(b.rarity); + case 'type': + return a.type.localeCompare(b.type); + default: + return 0; + } + }); + + const totalValue = cards.reduce((sum, card) => sum + (card.cost * card.quantity), 0); + const totalCards = cards.reduce((sum, card) => sum + card.quantity, 0); + + if (loading) { + return ( + +
+
+
+
+ ); + } + + if (!collection) { + return ( + +
+
+

+ Collection not found +

+ +
+
+
+ ); + } + + return ( + + {/* Hero Section with Collection Info */} +
+
+
+
+
👑
+
+
+

+ {collection.name} +

+
+
+
+ 👤 +
+ Crafted by {collection.creator} +
+ + Format: {collection.format} + + Cost: {collection.cost} + + Appears in: {collection.playGuide} + + +4 more + +
+
+
+ +
+ Created {collection.createdAt} + + Last updated {collection.lastUpdated} +
+
+
+ + {/* Action Buttons */} +
+
+
+
+ + + + + +
+ + +
+
+
+ + {/* Stats and Filters */} +
+ {/* Collection Stats */} +
+
+
{totalCards}
+
Total Cards
+
+
+
${totalValue.toFixed(2)}
+
Total Value
+
+
+
{collection.views}
+
Views
+
+
+
{collection.favorites}
+
Favorites
+
+
+ + {/* Filters */} +
+
+
+ setSearchQuery(e.target.value)} + /> +
+ +
+ + + + + + +
+ + +
+
+
+
+ + {/* Cards Display */} +
+

+ Cards ({sortedCards.length}) +

+
+ + {viewMode === 'grid' ? ( +
+ {sortedCards.map(card => ( +
router.push(`/card/${card.id}`)} + > +
+ {card.name} { + e.target.src = 'https://via.placeholder.com/250x350/6366f1/ffffff?text=No+Image'; + }} + /> + {card.quantity > 1 && ( +
+ {card.quantity}x +
+ )} +
+
+ {card.name} +
+
+ ${card.cost} +
+
+
+
+ ))} +
+ ) : ( +
+ {sortedCards.map(card => ( +
router.push(`/card/${card.id}`)} + > + {card.name} { + e.target.src = 'https://via.placeholder.com/64x88/6366f1/ffffff?text=No+Image'; + }} + /> +
+

+ {card.name} +

+
+ {card.set} • {card.rarity} • {card.type} +
+
+
+
+ ${card.cost} +
+
+ Qty: {card.quantity} +
+
+
+ ))} +
+ )} + + {sortedCards.length === 0 && ( +
+
🔍
+

+ No cards found +

+

+ Try adjusting your search or filter criteria +

+
+ )} +
+ + {/* Share Modal */} + {showShareModal && ( +
+
+

+ Share Collection +

+
+
+ +
+ + +
+
+
+
+ +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/pages/collections.js b/pages/collections.js index 150210e..ced946d 100644 --- a/pages/collections.js +++ b/pages/collections.js @@ -267,7 +267,11 @@ export default function Collections() {
{collections.map(collection => ( -
+
router.push(`/collection/${collection.id}`)} + >