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 (
+
+ Try adjusting your search or filter criteria
+
+ Collection not found
+
+
+
+ {collection.name}
+
+
+ Cards ({sortedCards.length})
+
+ {
+ e.target.src = 'https://via.placeholder.com/250x350/6366f1/ffffff?text=No+Image';
+ }}
+ />
+ {card.quantity > 1 && (
+
{
+ e.target.src = 'https://via.placeholder.com/64x88/6366f1/ffffff?text=No+Image';
+ }}
+ />
+
+ {card.name}
+
+
+ No cards found
+
+
+ Share Collection
+
+