From 49cfa31253139c950fa20e121db1846b674d7eda Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 23 Jul 2025 06:57:22 -0500 Subject: [PATCH] Consolidate API functions to stay within Vercel Hobby plan limits - reduce from 12+ to 8 functions --- api/admin/{load-cards.js => index.js} | 198 ++++++++++------ api/admin/users.js | 214 ----------------- api/cards/find-or-create.js | 169 -------------- api/cards/{search.js => index.js} | 111 ++++++++- api/collections/add-card.js | 174 -------------- api/collections/index.js | 322 ++++++++++++++++++-------- src/components/admin/CardLoader.tsx | 7 +- src/services/cardDataSources.ts | 2 +- 8 files changed, 467 insertions(+), 730 deletions(-) rename api/admin/{load-cards.js => index.js} (77%) delete mode 100644 api/admin/users.js delete mode 100644 api/cards/find-or-create.js rename api/cards/{search.js => index.js} (54%) delete mode 100644 api/collections/add-card.js diff --git a/api/admin/load-cards.js b/api/admin/index.js similarity index 77% rename from api/admin/load-cards.js rename to api/admin/index.js index a9077e1..c2e059c 100644 --- a/api/admin/load-cards.js +++ b/api/admin/index.js @@ -281,56 +281,7 @@ async function loadLorcanaCards() { } } -// POST /api/admin/load-cards - Load cards from external APIs -export async function POST(request) { - try { - const token = request.headers.get('authorization')?.replace('Bearer ', ''); - const user = await verifyToken(token); - - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - // Check if user is admin (you can implement your own admin check) - const { searchParams } = new URL(request.url); - const game = searchParams.get('game'); // 'MTG', 'POKEMON', 'LORCANA', or 'ALL' - - console.log(`🚀 Starting card loading process for game: ${game}`); - - let results = {}; - - if (game === 'MTG' || game === 'ALL') { - results.mtg = await loadMTGCards(); - } - - if (game === 'POKEMON' || game === 'ALL') { - results.pokemon = await loadPokemonCards(); - } - - if (game === 'LORCANA' || game === 'ALL') { - results.lorcana = await loadLorcanaCards(); - } - - const totalLoaded = Object.values(results).reduce((sum, count) => sum + count, 0); - - console.log(`🎉 Card loading completed! Total loaded: ${totalLoaded}`); - - return NextResponse.json({ - success: true, - message: `Successfully loaded ${totalLoaded} cards`, - results - }); - - } catch (error) { - console.error('❌ Error in card loading API:', error); - return NextResponse.json( - { error: 'Failed to load cards', details: error.message }, - { status: 500 } - ); - } -} - -// GET /api/admin/load-cards - Get loading status +// GET /api/admin - Get admin data (card counts, user stats) export async function GET(request) { try { const token = request.headers.get('authorization')?.replace('Bearer ', ''); @@ -340,30 +291,139 @@ export async function GET(request) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - // Get card counts from database - const result = await sql.query(` - SELECT - game, - COUNT(*) as count - FROM cards - GROUP BY game - `); + const { searchParams } = new URL(request.url); + const action = searchParams.get('action'); - const counts = {}; - result.rows.forEach(row => { - counts[row.game] = parseInt(row.count); - }); + if (action === 'card-counts') { + // Get card counts from database + const result = await sql.query(` + SELECT + game, + COUNT(*) as count + FROM cards + GROUP BY game + `); + + const counts = {}; + result.rows.forEach(row => { + counts[row.game] = parseInt(row.count); + }); + + return NextResponse.json({ + success: true, + counts, + total: Object.values(counts).reduce((sum, count) => sum + count, 0) + }); + } - return NextResponse.json({ - success: true, - counts, - total: Object.values(counts).reduce((sum, count) => sum + count, 0) - }); + if (action === 'user-stats') { + // Get user statistics + const result = await sql.query(` + SELECT + COUNT(*) as total_users, + COUNT(CASE WHEN created_at >= NOW() - INTERVAL '7 days' THEN 1 END) as new_users_7d, + COUNT(CASE WHEN created_at >= NOW() - INTERVAL '30 days' THEN 1 END) as new_users_30d + FROM user_preferences + `); + + return NextResponse.json({ + success: true, + stats: result.rows[0] + }); + } + + return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); } catch (error) { - console.error('❌ Error getting card counts:', error); + console.error('❌ Error in admin GET:', error); return NextResponse.json( - { error: 'Failed to get card counts', details: error.message }, + { error: 'Failed to get admin data', details: error.message }, + { status: 500 } + ); + } +} + +// POST /api/admin - Load cards or manage users +export async function POST(request) { + try { + const token = request.headers.get('authorization')?.replace('Bearer ', ''); + const user = await verifyToken(token); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const action = searchParams.get('action'); + + if (action === 'load-cards') { + const { game } = await request.json(); + + console.log(`🚀 Starting card loading process for game: ${game}`); + + let results = {}; + + if (game === 'MTG' || game === 'ALL') { + results.mtg = await loadMTGCards(); + } + + if (game === 'POKEMON' || game === 'ALL') { + results.pokemon = await loadPokemonCards(); + } + + if (game === 'LORCANA' || game === 'ALL') { + results.lorcana = await loadLorcanaCards(); + } + + const totalLoaded = Object.values(results).reduce((sum, count) => sum + count, 0); + + console.log(`🎉 Card loading completed! Total loaded: ${totalLoaded}`); + + return NextResponse.json({ + success: true, + message: `Successfully loaded ${totalLoaded} cards`, + results + }); + } + + if (action === 'manage-users') { + const { operation, userId, data } = await request.json(); + + if (operation === 'promote') { + await sql.query(` + UPDATE user_preferences + SET roles = array_append(roles, 'admin') + WHERE user_id = $1 + `, [userId]); + + return NextResponse.json({ + success: true, + message: 'User promoted to admin' + }); + } + + if (operation === 'demote') { + await sql.query(` + UPDATE user_preferences + SET roles = array_remove(roles, 'admin') + WHERE user_id = $1 + `, [userId]); + + return NextResponse.json({ + success: true, + message: 'User demoted from admin' + }); + } + + return NextResponse.json({ error: 'Invalid operation' }, { status: 400 }); + } + + return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); + + } catch (error) { + console.error('❌ Error in admin POST:', error); + return NextResponse.json( + { error: 'Failed to process admin action', details: error.message }, { status: 500 } ); } diff --git a/api/admin/users.js b/api/admin/users.js deleted file mode 100644 index d373b5a..0000000 --- a/api/admin/users.js +++ /dev/null @@ -1,214 +0,0 @@ -const { Pool } = require('pg'); -const jwt = require('jsonwebtoken'); -const url = require('url'); - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, - ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, -}); - -// Middleware to verify admin access -function verifyAdmin(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); - - // Check if user has admin role - if (!decoded.roles || !decoded.roles.includes('admin')) { - throw new Error('Admin access required'); - } - - return decoded; - } catch (error) { - throw new Error('Invalid token or insufficient permissions'); - } -} - -export default async function handler(req, res) { - const client = await pool.connect(); - - try { - // Verify admin access - verifyAdmin(req); - - if (req.method === 'GET') { - // Parse query parameters - const parsedUrl = url.parse(req.url, true); - const query = parsedUrl.query; - - const page = parseInt(query.page || '1'); - const limit = parseInt(query.limit || '20'); - const search = query.search || ''; - const offset = (page - 1) * limit; - - let whereClause = ''; - let queryParams = [limit, offset]; - - if (search) { - whereClause = 'WHERE u.username ILIKE $3 OR u.email ILIKE $3 OR u.first_name ILIKE $3 OR u.last_name ILIKE $3'; - queryParams.push(`%${search}%`); - } - - // Get users with their roles - const usersQuery = ` - SELECT - u.id, u.username, u.email, u.first_name, u.last_name, - u.is_active, u.created_at, u.last_login, - ARRAY_AGG(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) as roles - FROM users u - LEFT JOIN user_roles ur ON u.id = ur.user_id - LEFT JOIN roles r ON ur.role_id = r.id - ${whereClause} - GROUP BY u.id, u.username, u.email, u.first_name, u.last_name, u.is_active, u.created_at, u.last_login - ORDER BY u.created_at DESC - LIMIT $1 OFFSET $2 - `; - - const usersResult = await client.query(usersQuery, queryParams); - - // Get total count - let countQuery = 'SELECT COUNT(*) FROM users u'; - let countParams = []; - - if (search) { - countQuery += ' WHERE u.username ILIKE $1 OR u.email ILIKE $1 OR u.first_name ILIKE $1 OR u.last_name ILIKE $1'; - countParams.push(`%${search}%`); - } - - const countResult = await client.query(countQuery, countParams); - const total = parseInt(countResult.rows[0].count); - - res.status(200).json({ - success: true, - users: usersResult.rows.map(user => ({ - id: user.id, - username: user.username, - email: user.email, - firstName: user.first_name, - lastName: user.last_name, - isActive: user.is_active, - roles: user.roles || [], - createdAt: user.created_at, - lastLogin: user.last_login - })), - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } - }); - - } else if (req.method === 'PUT') { - // Update user (activate/deactivate, change roles) - const parsedUrl = url.parse(req.url, true); - const userId = parsedUrl.query.id; - - if (!userId) { - return res.status(400).json({ error: 'User ID required' }); - } - - // Parse request body - const { isActive, roles } = req.body; - - // Update user status - if (typeof isActive === 'boolean') { - await client.query( - 'UPDATE users SET is_active = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', - [isActive, userId] - ); - } - - // Update user roles - if (roles && Array.isArray(roles)) { - // Remove existing roles - await client.query('DELETE FROM user_roles WHERE user_id = $1', [userId]); - - // Add new roles - for (const roleName of roles) { - const roleResult = await client.query('SELECT id FROM roles WHERE name = $1', [roleName]); - if (roleResult.rows.length > 0) { - await client.query( - 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)', - [userId, roleResult.rows[0].id] - ); - } - } - } - - // Get updated user data - const updatedUserQuery = ` - SELECT - u.id, u.username, u.email, u.first_name, u.last_name, - u.is_active, u.created_at, u.last_login, - ARRAY_AGG(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) as roles - FROM users u - LEFT JOIN user_roles ur ON u.id = ur.user_id - LEFT JOIN roles r ON ur.role_id = r.id - WHERE u.id = $1 - GROUP BY u.id, u.username, u.email, u.first_name, u.last_name, u.is_active, u.created_at, u.last_login - `; - - const updatedUser = await client.query(updatedUserQuery, [userId]); - - res.status(200).json({ - success: true, - message: 'User updated successfully', - user: { - id: updatedUser.rows[0].id, - username: updatedUser.rows[0].username, - email: updatedUser.rows[0].email, - firstName: updatedUser.rows[0].first_name, - lastName: updatedUser.rows[0].last_name, - isActive: updatedUser.rows[0].is_active, - roles: updatedUser.rows[0].roles || [], - createdAt: updatedUser.rows[0].created_at, - lastLogin: updatedUser.rows[0].last_login - } - }); - - } else if (req.method === 'DELETE') { - // Delete user (soft delete - deactivate) - const parsedUrl = url.parse(req.url, true); - const userId = parsedUrl.query.id; - - if (!userId) { - return res.status(400).json({ error: 'User ID required' }); - } - - await client.query( - 'UPDATE users SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1', - [userId] - ); - - res.status(200).json({ - success: true, - message: 'User deactivated successfully' - }); - - } else { - res.status(405).json({ error: 'Method not allowed' }); - } - - } catch (error) { - console.error('Admin users API error:', error); - - if (error.message === 'No token provided' || error.message === 'Invalid token or insufficient permissions' || error.message === 'Admin access required') { - res.status(401).json({ error: error.message }); - } else { - res.status(500).json({ - error: 'Internal server error', - details: error.message - }); - } - } finally { - client.release(); - } -} \ No newline at end of file diff --git a/api/cards/find-or-create.js b/api/cards/find-or-create.js deleted file mode 100644 index 4e9a792..0000000 --- a/api/cards/find-or-create.js +++ /dev/null @@ -1,169 +0,0 @@ -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(); - } -} \ No newline at end of file diff --git a/api/cards/search.js b/api/cards/index.js similarity index 54% rename from api/cards/search.js rename to api/cards/index.js index 679648a..b3b7b45 100644 --- a/api/cards/search.js +++ b/api/cards/index.js @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server'; import { sql } from '@vercel/postgres'; +import { verifyToken } from '../setup-auth.js'; -// GET /api/cards/search - Search cards from database +// GET /api/cards - Search cards from database export async function GET(request) { try { const { searchParams } = new URL(request.url); @@ -155,4 +156,112 @@ export async function GET(request) { { status: 500 } ); } +} + +// POST /api/cards - Find or create card +export async function POST(request) { + try { + const token = request.headers.get('authorization')?.replace('Bearer ', ''); + const user = await verifyToken(token); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { name, game, set_name, set_code, card_number } = await request.json(); + + if (!name || !game) { + return NextResponse.json({ error: 'Name and game are required' }, { status: 400 }); + } + + console.log(`🔍 Finding or creating card: "${name}" (${game})`); + + // Try to find existing card + let result = await sql.query(` + SELECT * FROM cards + WHERE name = $1 AND game = $2 + ORDER BY created_at DESC + LIMIT 1 + `, [name, game]); + + if (result.rows.length > 0) { + const card = result.rows[0]; + console.log(`✅ Found existing card: ${card.name}`); + + return NextResponse.json({ + success: true, + data: { + id: card.id, + name: card.name, + set_name: card.set_name, + set_code: card.set_code, + card_number: card.card_number, + rarity: card.rarity, + game: card.game, + mana_cost: card.mana_cost, + cmc: card.cmc, + card_type: card.card_type, + colors: card.colors ? JSON.parse(card.colors) : [], + oracle_text: card.oracle_text, + power: card.power, + toughness: card.toughness, + image_url: card.image_url, + stock_image_url: card.stock_image_url, + current_price: card.current_price, + market_price: card.market_price, + verified: card.verified, + createdAt: card.created_at, + updatedAt: card.updated_at, + }, + message: 'Card found' + }); + } + + // Create new card if not found + console.log(`➕ Creating new card: ${name}`); + + result = await sql.query(` + INSERT INTO cards ( + name, game, set_name, set_code, card_number, verified + ) VALUES ($1, $2, $3, $4, $5, $6) + RETURNING * + `, [name, game, set_name || '', set_code || '', card_number || '', false]); + + const newCard = result.rows[0]; + + return NextResponse.json({ + success: true, + data: { + id: newCard.id, + name: newCard.name, + set_name: newCard.set_name, + set_code: newCard.set_code, + card_number: newCard.card_number, + rarity: newCard.rarity, + game: newCard.game, + mana_cost: newCard.mana_cost, + cmc: newCard.cmc, + card_type: newCard.card_type, + colors: newCard.colors ? JSON.parse(newCard.colors) : [], + oracle_text: newCard.oracle_text, + power: newCard.power, + toughness: newCard.toughness, + image_url: newCard.image_url, + stock_image_url: newCard.stock_image_url, + current_price: newCard.current_price, + market_price: newCard.market_price, + verified: newCard.verified, + createdAt: newCard.created_at, + updatedAt: newCard.updated_at, + }, + message: 'Card created' + }); + + } catch (error) { + console.error('❌ Error finding/creating card:', error); + return NextResponse.json( + { error: 'Failed to find/create card', details: error.message }, + { status: 500 } + ); + } } \ No newline at end of file diff --git a/api/collections/add-card.js b/api/collections/add-card.js deleted file mode 100644 index 1fd100d..0000000 --- a/api/collections/add-card.js +++ /dev/null @@ -1,174 +0,0 @@ -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(); - } -} \ No newline at end of file diff --git a/api/collections/index.js b/api/collections/index.js index 73397e9..1b95a09 100644 --- a/api/collections/index.js +++ b/api/collections/index.js @@ -1,121 +1,245 @@ -const { Pool } = require('pg'); -const jwt = require('jsonwebtoken'); +import { NextResponse } from 'next/server'; +import { sql } from '@vercel/postgres'; +import { verifyToken } from '../setup-auth.js'; -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'; - +// GET /api/collections - Get user collections +export async function GET(request) { try { - const decoded = jwt.verify(token, jwtSecret); - return decoded; + const token = request.headers.get('authorization')?.replace('Bearer ', ''); + const user = await verifyToken(token); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const collectionId = searchParams.get('id'); + + if (collectionId) { + // Get specific collection with cards + const result = await sql.query(` + SELECT + c.id, + c.name, + c.description, + c.is_public, + c.created_at, + c.updated_at, + cc.quantity, + cc.condition, + cc.notes, + cc.purchase_price, + cc.purchase_date, + card.id as card_id, + card.name as card_name, + card.set_name, + card.set_code, + card.card_number, + card.rarity, + card.game, + card.card_type, + card.mana_cost, + card.cmc, + card.colors, + card.oracle_text, + card.power, + card.toughness, + card.image_url, + card.stock_image_url, + card.current_price, + card.market_price + FROM user_collections c + LEFT JOIN collection_cards cc ON c.id = cc.collection_id + LEFT JOIN cards card ON cc.card_id = card.id + WHERE c.id = $1 AND c.user_id = $2 + ORDER BY card.name ASC + `, [collectionId, user.id]); + + if (result.rows.length === 0) { + return NextResponse.json({ error: 'Collection not found' }, { status: 404 }); + } + + const collection = { + id: result.rows[0].id, + name: result.rows[0].name, + description: result.rows[0].description, + is_public: result.rows[0].is_public, + created_at: result.rows[0].created_at, + updated_at: result.rows[0].updated_at, + cards: result.rows + .filter(row => row.card_id) + .map(row => ({ + quantity: row.quantity, + condition: row.condition, + notes: row.notes, + purchase_price: row.purchase_price, + purchase_date: row.purchase_date, + card: { + id: row.card_id, + name: row.card_name, + set_name: row.set_name, + set_code: row.set_code, + card_number: row.card_number, + rarity: row.rarity, + game: row.game, + card_type: row.card_type, + mana_cost: row.mana_cost, + cmc: row.cmc, + colors: row.colors ? JSON.parse(row.colors) : [], + oracle_text: row.oracle_text, + power: row.power, + toughness: row.toughness, + image_url: row.image_url, + stock_image_url: row.stock_image_url, + current_price: row.current_price, + market_price: row.market_price + } + })) + }; + + return NextResponse.json({ + success: true, + data: collection + }); + } else { + // Get all user collections + const result = await sql.query(` + SELECT + c.id, + c.name, + c.description, + c.is_public, + c.created_at, + c.updated_at, + COUNT(cc.card_id) as card_count + FROM user_collections c + LEFT JOIN collection_cards cc ON c.id = cc.collection_id + WHERE c.user_id = $1 + GROUP BY c.id, c.name, c.description, c.is_public, c.created_at, c.updated_at + ORDER BY c.created_at DESC + `, [user.id]); + + return NextResponse.json({ + success: true, + data: result.rows.map(row => ({ + id: row.id, + name: row.name, + description: row.description, + is_public: row.is_public, + created_at: row.created_at, + updated_at: row.updated_at, + card_count: parseInt(row.card_count) + })) + }); + } } catch (error) { - throw new Error('Invalid token'); + console.error('Error fetching collections:', error); + return NextResponse.json( + { error: 'Failed to fetch collections', details: error.message }, + { status: 500 } + ); } } -export default async function handler(req, res) { - const client = await pool.connect(); - +// POST /api/collections - Create collection or add card to collection +export async function POST(request) { try { - // Verify authentication - const user = verifyAuth(req); + const token = request.headers.get('authorization')?.replace('Bearer ', ''); + const user = await verifyToken(token); + + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } - 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 { searchParams } = new URL(request.url); + const action = searchParams.get('action'); - 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') { + if (action === 'create') { // 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 { name, description, is_public } = await request.json(); + + if (!name) { + return NextResponse.json({ error: 'Collection name is required' }, { status: 400 }); } - const insertQuery = ` + const result = await sql.query(` 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 - `; + RETURNING * + `, [user.id, name, description || '', is_public || false]); - const result = await client.query(insertQuery, [ - user.userId, - name.trim(), - description?.trim() || null, - isPublic - ]); - - const newCollection = result.rows[0]; - - res.status(201).json({ + return NextResponse.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 - } + data: result.rows[0], + message: 'Collection created successfully' }); - - } else { - res.status(405).json({ error: 'Method not allowed' }); } + if (action === 'add-card') { + // Add card to collection + const { collection_id, card_id, quantity, condition, notes, purchase_price, purchase_date } = await request.json(); + + if (!collection_id || !card_id) { + return NextResponse.json({ error: 'Collection ID and card ID are required' }, { status: 400 }); + } + + // Verify collection belongs to user + const collectionCheck = await sql.query(` + SELECT id FROM user_collections WHERE id = $1 AND user_id = $2 + `, [collection_id, user.id]); + + if (collectionCheck.rows.length === 0) { + return NextResponse.json({ error: 'Collection not found or access denied' }, { status: 404 }); + } + + // Check if card already exists in collection + const existingCard = await sql.query(` + SELECT id, quantity FROM collection_cards + WHERE collection_id = $1 AND card_id = $2 + `, [collection_id, card_id]); + + if (existingCard.rows.length > 0) { + // Update existing card quantity + const newQuantity = (existingCard.rows[0].quantity || 0) + (quantity || 1); + await sql.query(` + UPDATE collection_cards + SET quantity = $1, updated_at = NOW() + WHERE id = $2 + `, [newQuantity, existingCard.rows[0].id]); + + return NextResponse.json({ + success: true, + message: 'Card quantity updated in collection' + }); + } else { + // Add new card to collection + await sql.query(` + INSERT INTO collection_cards ( + collection_id, card_id, quantity, condition, notes, purchase_price, purchase_date + ) VALUES ($1, $2, $3, $4, $5, $6, $7) + `, [ + collection_id, + card_id, + quantity || 1, + condition || 'near-mint', + notes || '', + purchase_price || null, + purchase_date || null + ]); + + return NextResponse.json({ + success: true, + message: 'Card added to collection' + }); + } + } + + return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); } 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(); + console.error('Error with collections:', error); + return NextResponse.json( + { error: 'Failed to process collection action', details: error.message }, + { status: 500 } + ); } } \ No newline at end of file diff --git a/src/components/admin/CardLoader.tsx b/src/components/admin/CardLoader.tsx index 02753ca..9f69451 100644 --- a/src/components/admin/CardLoader.tsx +++ b/src/components/admin/CardLoader.tsx @@ -25,7 +25,7 @@ const CardLoader: React.FC = () => { // Fetch current card counts const fetchCardCounts = async () => { try { - const response = await fetch('https://tcg-vault.vercel.app/api/admin/load-cards', { + const response = await fetch('https://tcg-vault.vercel.app/api/admin?action=card-counts', { headers: { 'Authorization': `Bearer ${token}` } @@ -51,12 +51,13 @@ const CardLoader: React.FC = () => { setLoadingResults({}); try { - const response = await fetch(`https://tcg-vault.vercel.app/api/admin/load-cards?game=${game}`, { + const response = await fetch(`https://tcg-vault.vercel.app/api/admin?action=load-cards`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' - } + }, + body: JSON.stringify({ game }) }); if (response.ok) { diff --git a/src/services/cardDataSources.ts b/src/services/cardDataSources.ts index 0fc2db1..9431d8a 100644 --- a/src/services/cardDataSources.ts +++ b/src/services/cardDataSources.ts @@ -756,7 +756,7 @@ export const cardDataService = { params.append('game', game); } - const response = await fetch(`https://tcg-vault.vercel.app/api/cards/search?${params}`); + const response = await fetch(`https://tcg-vault.vercel.app/api/cards?${params}`); if (!response.ok) { throw new Error(`Database search failed: ${response.status}`);