Consolidate API functions to stay within Vercel Hobby plan limits - reduce from 12+ to 8 functions
This commit is contained in:
parent
7057417a89
commit
49cfa31253
8 changed files with 467 additions and 730 deletions
|
|
@ -281,56 +281,7 @@ async function loadLorcanaCards() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/admin/load-cards - Load cards from external APIs
|
// GET /api/admin - Get admin data (card counts, user stats)
|
||||||
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
|
|
||||||
export async function GET(request) {
|
export async function GET(request) {
|
||||||
try {
|
try {
|
||||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||||
|
|
@ -340,30 +291,139 @@ export async function GET(request) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get card counts from database
|
const { searchParams } = new URL(request.url);
|
||||||
const result = await sql.query(`
|
const action = searchParams.get('action');
|
||||||
SELECT
|
|
||||||
game,
|
|
||||||
COUNT(*) as count
|
|
||||||
FROM cards
|
|
||||||
GROUP BY game
|
|
||||||
`);
|
|
||||||
|
|
||||||
const counts = {};
|
if (action === 'card-counts') {
|
||||||
result.rows.forEach(row => {
|
// Get card counts from database
|
||||||
counts[row.game] = parseInt(row.count);
|
const result = await sql.query(`
|
||||||
});
|
SELECT
|
||||||
|
game,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM cards
|
||||||
|
GROUP BY game
|
||||||
|
`);
|
||||||
|
|
||||||
return NextResponse.json({
|
const counts = {};
|
||||||
success: true,
|
result.rows.forEach(row => {
|
||||||
counts,
|
counts[row.game] = parseInt(row.count);
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('❌ Error getting card counts:', error);
|
console.error('❌ Error in admin GET:', error);
|
||||||
return NextResponse.json(
|
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 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { sql } from '@vercel/postgres';
|
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) {
|
export async function GET(request) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +1,245 @@
|
||||||
const { Pool } = require('pg');
|
import { NextResponse } from 'next/server';
|
||||||
const jwt = require('jsonwebtoken');
|
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 {
|
try {
|
||||||
const decoded = jwt.verify(token, jwtSecret);
|
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||||
return decoded;
|
const user = await verifyToken(token);
|
||||||
} catch (error) {
|
|
||||||
throw new Error('Invalid token');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
if (!user) {
|
||||||
const client = await pool.connect();
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const { searchParams } = new URL(request.url);
|
||||||
// Verify authentication
|
const collectionId = searchParams.get('id');
|
||||||
const user = verifyAuth(req);
|
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (collectionId) {
|
||||||
// Get user's collections
|
// Get specific collection with cards
|
||||||
const collectionsQuery = `
|
const result = await sql.query(`
|
||||||
SELECT
|
SELECT
|
||||||
uc.id, uc.name, uc.description, uc.is_public, uc.created_at, uc.updated_at,
|
c.id,
|
||||||
COUNT(cc.id) as total_cards,
|
c.name,
|
||||||
COALESCE(SUM(cc.quantity * COALESCE(c.current_price, 0)), 0) as total_value
|
c.description,
|
||||||
FROM user_collections uc
|
c.is_public,
|
||||||
LEFT JOIN collection_cards cc ON uc.id = cc.collection_id
|
c.created_at,
|
||||||
LEFT JOIN cards c ON cc.card_id = c.id
|
c.updated_at,
|
||||||
WHERE uc.user_id = $1
|
cc.quantity,
|
||||||
GROUP BY uc.id, uc.name, uc.description, uc.is_public, uc.created_at, uc.updated_at
|
cc.condition,
|
||||||
ORDER BY uc.created_at DESC
|
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]);
|
if (result.rows.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
|
||||||
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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertQuery = `
|
const collection = {
|
||||||
INSERT INTO user_collections (user_id, name, description, is_public)
|
id: result.rows[0].id,
|
||||||
VALUES ($1, $2, $3, $4)
|
name: result.rows[0].name,
|
||||||
RETURNING id, name, description, is_public, created_at, updated_at
|
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, [
|
return NextResponse.json({
|
||||||
user.userId,
|
|
||||||
name.trim(),
|
|
||||||
description?.trim() || null,
|
|
||||||
isPublic
|
|
||||||
]);
|
|
||||||
|
|
||||||
const newCollection = result.rows[0];
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Collection created successfully',
|
data: collection
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Collections API error:', error);
|
console.error('Error fetching collections:', error);
|
||||||
|
return NextResponse.json(
|
||||||
if (error.message === 'No token provided' || error.message === 'Invalid token') {
|
{ error: 'Failed to fetch collections', details: error.message },
|
||||||
res.status(401).json({ error: error.message });
|
{ status: 500 }
|
||||||
} else {
|
);
|
||||||
res.status(500).json({
|
}
|
||||||
error: 'Internal server error',
|
}
|
||||||
details: error.message
|
|
||||||
});
|
// POST /api/collections - Create collection or add card to collection
|
||||||
}
|
export async function POST(request) {
|
||||||
} finally {
|
try {
|
||||||
client.release();
|
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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ const CardLoader: React.FC = () => {
|
||||||
// Fetch current card counts
|
// Fetch current card counts
|
||||||
const fetchCardCounts = async () => {
|
const fetchCardCounts = async () => {
|
||||||
try {
|
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: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
|
|
@ -51,12 +51,13 @@ const CardLoader: React.FC = () => {
|
||||||
setLoadingResults({});
|
setLoadingResults({});
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
},
|
||||||
|
body: JSON.stringify({ game })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
|
||||||
|
|
@ -756,7 +756,7 @@ export const cardDataService = {
|
||||||
params.append('game', game);
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Database search failed: ${response.status}`);
|
throw new Error(`Database search failed: ${response.status}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue