Consolidate API functions to stay within Vercel Hobby plan limits - reduce from 12+ to 8 functions

This commit is contained in:
Randall Stillwell 2025-07-23 06:57:22 -05:00
parent 7057417a89
commit 49cfa31253
8 changed files with 467 additions and 730 deletions

View file

@ -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
`);
return NextResponse.json({
success: true,
counts,
total: Object.values(counts).reduce((sum, count) => sum + count, 0)
});
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)
});
}
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 }
);
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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);
@ -156,3 +157,111 @@ export async function GET(request) {
);
}
}
// 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 }
);
}
}

View file

@ -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();
}
}

View file

@ -1,121 +1,245 @@
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';
import { NextResponse } from 'next/server';
import { sql } from '@vercel/postgres';
import { verifyToken } from '../setup-auth.js';
// GET /api/collections - Get user collections
export async function GET(request) {
try {
const decoded = jwt.verify(token, jwtSecret);
return decoded;
} catch (error) {
throw new Error('Invalid token');
}
}
const token = request.headers.get('authorization')?.replace('Bearer ', '');
const user = await verifyToken(token);
export default async function handler(req, res) {
const client = await pool.connect();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
// Verify authentication
const user = verifyAuth(req);
const { searchParams } = new URL(request.url);
const collectionId = searchParams.get('id');
if (req.method === 'GET') {
// Get user's collections
const collectionsQuery = `
if (collectionId) {
// Get specific collection with cards
const result = await sql.query(`
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
`;
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]);
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' });
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
}
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 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
}
}))
};
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: collection
});
} else {
res.status(405).json({ error: 'Method not allowed' });
}
// 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) {
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 fetching collections:', error);
return NextResponse.json(
{ error: 'Failed to fetch collections', details: error.message },
{ status: 500 }
);
}
}
// POST /api/collections - Create collection or add card to collection
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 === 'create') {
// Create new collection
const { name, description, is_public } = await request.json();
if (!name) {
return NextResponse.json({ error: 'Collection name is required' }, { status: 400 });
}
const result = await sql.query(`
INSERT INTO user_collections (user_id, name, description, is_public)
VALUES ($1, $2, $3, $4)
RETURNING *
`, [user.id, name, description || '', is_public || false]);
return NextResponse.json({
success: true,
data: result.rows[0],
message: 'Collection created successfully'
});
}
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('Error with collections:', error);
return NextResponse.json(
{ error: 'Failed to process collection action', details: error.message },
{ status: 500 }
);
}
}

View file

@ -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) {

View file

@ -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}`);