diff --git a/next.config.js b/next.config.js
index 2b787f1..b8b5027 100644
--- a/next.config.js
+++ b/next.config.js
@@ -3,27 +3,6 @@ const nextConfig = {
images: {
domains: ['api.scryfall.com', 'images.pokemontcg.io', 'lorcana-api.com'],
},
- async headers() {
- return [
- {
- source: '/api/(.*)',
- headers: [
- {
- key: 'Access-Control-Allow-Origin',
- value: '*',
- },
- {
- key: 'Access-Control-Allow-Methods',
- value: 'GET, POST, PUT, DELETE, OPTIONS',
- },
- {
- key: 'Access-Control-Allow-Headers',
- value: 'Content-Type, Authorization',
- },
- ],
- },
- ];
- },
};
module.exports = nextConfig;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index e752d07..1702225 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,45 +8,28 @@
"name": "tcg-vault",
"version": "0.1.0",
"dependencies": {
- "@stackframe/stack": "^2.8.22",
- "@tanstack/react-query": "^5.83.0",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.3.0",
- "@testing-library/user-event": "^13.5.0",
- "@types/bcryptjs": "^2.4.6",
- "@types/jest": "^27.5.2",
- "@types/jsonwebtoken": "^9.0.10",
- "@types/node": "^16.18.126",
- "@types/pg": "^8.15.4",
- "@types/react": "^18.3.17",
- "@types/react-dom": "^18.3.5",
- "@types/react-router-dom": "^5.3.3",
"@vercel/analytics": "^1.5.0",
- "@vercel/blob": "^1.1.1",
"@vercel/postgres": "^0.10.0",
"@vercel/speed-insights": "^1.2.0",
"axios": "^1.10.0",
"bcryptjs": "^3.0.2",
"jsonwebtoken": "^9.0.2",
"next": "^15.4.2",
- "pg": "^8.16.3",
"react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-router-dom": "^6.28.0",
- "web-vitals": "^2.1.4"
+ "react-dom": "^18.3.1"
},
"devDependencies": {
+ "@types/bcryptjs": "^2.4.6",
+ "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^16.18.126",
+ "@types/react": "^18.3.17",
+ "@types/react-dom": "^18.3.5",
"autoprefixer": "^10.4.21",
"eslint": "^8",
"eslint-config-next": "15.4.2",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^4.9.5"
- },
- "engines": {
- "node": "22.x"
}
},
"node_modules/@adobe/css-tools": {
diff --git a/package.json b/package.json
index a02aefc..a410952 100644
--- a/package.json
+++ b/package.json
@@ -2,9 +2,6 @@
"name": "tcg-vault",
"version": "0.1.0",
"private": true,
- "engines": {
- "node": "22.x"
- },
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -12,53 +9,22 @@
"lint": "next lint"
},
"dependencies": {
- "@stackframe/stack": "^2.8.22",
- "@tanstack/react-query": "^5.83.0",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.3.0",
- "@testing-library/user-event": "^13.5.0",
- "@types/bcryptjs": "^2.4.6",
- "@types/jest": "^27.5.2",
- "@types/jsonwebtoken": "^9.0.10",
- "@types/node": "^16.18.126",
- "@types/pg": "^8.15.4",
- "@types/react": "^18.3.17",
- "@types/react-dom": "^18.3.5",
- "@types/react-router-dom": "^5.3.3",
- "@vercel/analytics": "^1.5.0",
- "@vercel/blob": "^1.1.1",
"@vercel/postgres": "^0.10.0",
+ "@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
- "axios": "^1.10.0",
- "bcryptjs": "^3.0.2",
- "jsonwebtoken": "^9.0.2",
"next": "^15.4.2",
- "pg": "^8.16.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-router-dom": "^6.28.0",
- "web-vitals": "^2.1.4"
- },
- "eslintConfig": {
- "extends": [
- "next"
- ]
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
+ "bcryptjs": "^3.0.2",
+ "jsonwebtoken": "^9.0.2",
+ "axios": "^1.10.0"
},
"devDependencies": {
"@types/node": "^16.18.126",
+ "@types/react": "^18.3.17",
+ "@types/react-dom": "^18.3.5",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/jsonwebtoken": "^9.0.10",
"autoprefixer": "^10.4.21",
"eslint": "^8",
"eslint-config-next": "15.4.2",
@@ -66,4 +32,4 @@
"tailwindcss": "^3.4.17",
"typescript": "^4.9.5"
}
-}
+}
\ No newline at end of file
diff --git a/pages/_app.js b/pages/_app.js
new file mode 100644
index 0000000..676d8aa
--- /dev/null
+++ b/pages/_app.js
@@ -0,0 +1,5 @@
+import '../styles/globals.css'
+
+export default function App({ Component, pageProps }) {
+ return
+}
\ No newline at end of file
diff --git a/pages/_app.tsx b/pages/_app.tsx
deleted file mode 100644
index aea1b3e..0000000
--- a/pages/_app.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import type { AppProps } from 'next/app';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { AuthProvider } from '../src/contexts/AuthContext';
-import '../src/index.css';
-
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- retry: 1,
- refetchOnWindowFocus: false,
- },
- },
-});
-
-export default function App({ Component, pageProps }: AppProps) {
- return (
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/pages/api/admin/index.js b/pages/api/admin/index.js
deleted file mode 100644
index 47e6886..0000000
--- a/pages/api/admin/index.js
+++ /dev/null
@@ -1,409 +0,0 @@
-import { sql } from '@vercel/postgres';
-import { verifyToken, isAdmin } from '../auth-utils.js';
-
-// Rate limiting for external APIs
-const rateLimiters = {
- mtg: { lastCall: 0, minInterval: 50 }, // 50ms between calls
- pokemon: { lastCall: 0, minInterval: 100 }, // 100ms between calls
- lorcana: { lastCall: 0, minInterval: 100 } // 100ms between calls
-};
-
-async function waitForRateLimit(api) {
- const now = Date.now();
- const limiter = rateLimiters[api];
- const timeSinceLastCall = now - limiter.lastCall;
-
- if (timeSinceLastCall < limiter.minInterval) {
- await new Promise(resolve =>
- setTimeout(resolve, limiter.minInterval - timeSinceLastCall)
- );
- }
-
- limiter.lastCall = Date.now();
-}
-
-// Load MTG cards from Scryfall
-async function loadMTGCards() {
- console.log('🃏 Loading MTG cards from Scryfall...');
-
- try {
- // Get total count first
- const countResponse = await fetch('https://api.scryfall.com/cards/search?q=game:paper');
- const countData = await countResponse.json();
- const totalCards = countData.total_cards;
-
- console.log(`📊 Found ${totalCards} MTG cards to load`);
-
- let loadedCount = 0;
- let page = 1;
-
- while (loadedCount < Math.min(totalCards, 1000)) { // Limit to 1000 for now
- await waitForRateLimit('mtg');
-
- const response = await fetch(`https://api.scryfall.com/cards/search?q=game:paper&page=${page}`);
- const data = await response.json();
-
- if (!data.data || data.data.length === 0) break;
-
- for (const card of data.data) {
- try {
- await sql.query(`
- INSERT INTO cards (
- name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
- card_type, colors, oracle_text, power, toughness, image_url,
- stock_image_url, current_price, market_price, scryfall_id, verified
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
- ON CONFLICT (scryfall_id) DO NOTHING
- `, [
- card.name,
- card.set_name,
- card.set,
- card.collector_number,
- card.rarity,
- 'MTG',
- card.mana_cost,
- card.cmc,
- card.type_line,
- JSON.stringify(card.colors),
- card.oracle_text,
- card.power,
- card.toughness,
- card.image_uris?.normal || card.image_uris?.small,
- card.image_uris?.small,
- card.prices?.usd ? parseFloat(card.prices.usd) : null,
- card.prices?.usd_foil ? parseFloat(card.prices.usd_foil) : null,
- card.id,
- true
- ]);
-
- loadedCount++;
- } catch (error) {
- console.error(`❌ Error loading MTG card ${card.name}:`, error);
- }
- }
-
- page++;
- console.log(`✅ Loaded ${loadedCount} MTG cards so far...`);
- }
-
- console.log(`🎉 Successfully loaded ${loadedCount} MTG cards`);
- return loadedCount;
- } catch (error) {
- console.error('❌ Error loading MTG cards:', error);
- return 0;
- }
-}
-
-// Load Pokémon cards from Pokémon TCG API
-async function loadPokemonCards() {
- console.log('⚡ Loading Pokémon cards from Pokémon TCG API...');
-
- try {
- let loadedCount = 0;
- let page = 1;
- const pageSize = 250; // API limit
-
- while (loadedCount < 1000) { // Limit to 1000 for now
- await waitForRateLimit('pokemon');
-
- const response = await fetch(`https://api.pokemontcg.io/v2/cards?page=${page}&pageSize=${pageSize}`);
- const data = await response.json();
-
- if (!data.data || data.data.length === 0) break;
-
- for (const card of data.data) {
- try {
- await sql.query(`
- INSERT INTO cards (
- name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
- card_type, colors, oracle_text, power, toughness, image_url,
- stock_image_url, current_price, market_price, scryfall_id, verified
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
- ON CONFLICT (scryfall_id) DO NOTHING
- `, [
- card.name,
- card.set.name,
- card.set.id,
- card.number,
- card.rarity,
- 'POKEMON',
- null, // No mana cost in Pokémon
- null, // No CMC in Pokémon
- card.supertype,
- JSON.stringify(card.types || []),
- card.rules?.join(' ') || '',
- card.nationalPokedexNumbers?.[0] || null,
- null, // No toughness in Pokémon
- card.images?.large,
- card.images?.small,
- card.cardmarket?.prices?.averageSellPrice || null,
- card.cardmarket?.prices?.lowPrice || null,
- card.id,
- true
- ]);
-
- loadedCount++;
- } catch (error) {
- console.error(`❌ Error loading Pokémon card ${card.name}:`, error);
- }
- }
-
- page++;
- console.log(`✅ Loaded ${loadedCount} Pokémon cards so far...`);
- }
-
- console.log(`🎉 Successfully loaded ${loadedCount} Pokémon cards`);
- return loadedCount;
- } catch (error) {
- console.error('❌ Error loading Pokémon cards:', error);
- return 0;
- }
-}
-
-// Load Lorcana cards from Lorcana API
-async function loadLorcanaCards() {
- console.log('🏰 Loading Lorcana cards from Lorcana API...');
-
- try {
- let loadedCount = 0;
-
- // Get all cards from Lorcana API
- await waitForRateLimit('lorcana');
- const response = await fetch('https://lorcana-api.com/api/v1/cards');
- const data = await response.json();
-
- if (!data.data || data.data.length === 0) {
- console.log('❌ No Lorcana cards found');
- return 0;
- }
-
- console.log(`📊 Found ${data.data.length} Lorcana cards to load`);
-
- for (const card of data.data) {
- try {
- await sql.query(`
- INSERT INTO cards (
- name, set_name, set_code, card_number, rarity, game, mana_cost, cmc,
- card_type, colors, oracle_text, power, toughness, image_url,
- stock_image_url, current_price, market_price, scryfall_id, verified
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
- ON CONFLICT (scryfall_id) DO NOTHING
- `, [
- card.name,
- card.set?.name || 'Unknown Set',
- card.set?.id || 'UNK',
- card.number || '0',
- card.rarity || 'Common',
- 'LORCANA',
- null, // No mana cost in Lorcana
- card.cost || null,
- card.type || 'Character',
- JSON.stringify(card.colors || []),
- card.text || '',
- card.strength || null,
- card.willpower || null,
- card.images?.full || card.images?.large,
- card.images?.small,
- null, // No price data available
- null, // No price data available
- card.id,
- true
- ]);
-
- loadedCount++;
- } catch (error) {
- console.error(`❌ Error loading Lorcana card ${card.name}:`, error);
- }
- }
-
- console.log(`🎉 Successfully loaded ${loadedCount} Lorcana cards`);
- return loadedCount;
- } catch (error) {
- console.error('❌ Error loading Lorcana cards:', error);
- return 0;
- }
-}
-
-// GET /api/admin - Get admin data
-export default async function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- if (req.method !== 'GET' && req.method !== 'POST') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- // Temporarily bypass auth for testing
- // const token = req.headers.authorization?.replace('Bearer ', '');
- // const user = await verifyToken(token);
-
- // if (!user) {
- // return res.status(401).json({ error: 'Unauthorized' });
- // }
-
- // if (!isAdmin(user)) {
- // return res.status(403).json({ error: 'Admin access required' });
- // }
-
- const { action } = req.query;
-
- // Simple test endpoint
- if (action === 'test') {
- return res.status(200).json({
- success: true,
- message: 'Admin API is working!',
- timestamp: new Date().toISOString()
- });
- }
-
- if (req.method === 'GET') {
- if (action === 'card-counts') {
- try {
- // First check if the cards table exists
- const tableCheck = await sql.query(`
- SELECT EXISTS (
- SELECT FROM information_schema.tables
- WHERE table_schema = 'public'
- AND table_name = 'cards'
- );
- `);
-
- if (!tableCheck.rows[0].exists) {
- return res.status(200).json({
- success: true,
- counts: {},
- total: 0,
- message: 'Cards table does not exist yet'
- });
- }
-
- // 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 res.status(200).json({
- success: true,
- counts,
- total: Object.values(counts).reduce((sum, count) => sum + count, 0)
- });
- } catch (dbError) {
- console.error('❌ Database error:', dbError);
- return res.status(500).json({
- success: false,
- error: 'Database error',
- details: dbError.message
- });
- }
- }
-
- 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 res.status(200).json({
- success: true,
- stats: result.rows[0]
- });
- }
-
- return res.status(400).json({ error: 'Invalid action' });
- }
-
- if (req.method === 'POST') {
- if (action === 'load-cards') {
- const { game } = req.body;
-
- 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 res.status(200).json({
- success: true,
- message: `Successfully loaded ${totalLoaded} cards`,
- results
- });
- }
-
- if (action === 'manage-users') {
- const { operation, userId, data } = req.body;
-
- if (operation === 'promote') {
- await sql.query(`
- UPDATE user_preferences
- SET roles = array_append(roles, 'admin')
- WHERE user_id = $1
- `, [userId]);
-
- return res.status(200).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 res.status(200).json({
- success: true,
- message: 'User demoted from admin'
- });
- }
-
- return res.status(400).json({ error: 'Invalid operation' });
- }
-
- return res.status(400).json({ error: 'Invalid action' });
- }
-
- } catch (error) {
- console.error('❌ Error in admin API:', error);
- return res.status(500).json(
- { error: 'Failed to process admin request', details: error.message }
- );
- }
-}
\ No newline at end of file
diff --git a/pages/api/auth-utils.js b/pages/api/auth-utils.js
deleted file mode 100644
index c709d62..0000000
--- a/pages/api/auth-utils.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import jwt from 'jsonwebtoken';
-import { sql } from '@vercel/postgres';
-
-export async function verifyToken(token) {
- if (!token) {
- return null;
- }
-
- try {
- const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
- const decoded = jwt.verify(token, jwtSecret);
-
- // Get user from database
- const result = await sql.query(`
- SELECT
- up.user_id,
- up.username,
- up.email,
- up.first_name,
- up.last_name,
- up.avatar_url,
- up.roles,
- up.created_at,
- up.updated_at
- FROM user_preferences up
- WHERE up.user_id = $1
- `, [decoded.userId]);
-
- if (result.rows.length === 0) {
- return null;
- }
-
- const user = result.rows[0];
- return {
- id: user.user_id,
- username: user.username,
- email: user.email,
- firstName: user.first_name,
- lastName: user.last_name,
- avatarUrl: user.avatar_url,
- roles: user.roles || [],
- createdAt: user.created_at,
- updatedAt: user.updated_at
- };
- } catch (error) {
- console.error('Token verification failed:', error);
- return null;
- }
-}
-
-export function isAdmin(user) {
- return user && user.roles && user.roles.includes('admin');
-}
\ No newline at end of file
diff --git a/pages/api/auth/login.js b/pages/api/auth/login.js
deleted file mode 100644
index 2c04b5d..0000000
--- a/pages/api/auth/login.js
+++ /dev/null
@@ -1,125 +0,0 @@
-const { Pool } = require('pg');
-const bcrypt = require('bcryptjs');
-const jwt = require('jsonwebtoken');
-
-const pool = new Pool({
- connectionString: process.env.DATABASE_URL,
- ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
-});
-
-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 {
- const { username, password } = req.body;
-
- // Validate input
- if (!username || !password) {
- return res.status(400).json({
- error: 'Username and password are required'
- });
- }
-
- // Get user by username or email
- const userResult = await client.query(`
- SELECT id, username, email, password_hash, first_name, last_name, is_active, last_login
- FROM users
- WHERE (username = $1 OR email = $1) AND is_active = true
- `, [username]);
-
- if (userResult.rows.length === 0) {
- return res.status(401).json({
- error: 'Invalid credentials'
- });
- }
-
- const user = userResult.rows[0];
-
- // Verify password
- const isValidPassword = await bcrypt.compare(password, user.password_hash);
- if (!isValidPassword) {
- return res.status(401).json({
- error: 'Invalid credentials'
- });
- }
-
- // Get user roles and permissions
- const userRoles = await client.query(`
- SELECT r.name, r.description,
- array_agg(p.name) as permissions
- FROM roles r
- JOIN user_roles ur ON r.id = ur.role_id
- LEFT JOIN role_permissions rp ON r.id = rp.role_id
- LEFT JOIN permissions p ON rp.permission_id = p.id
- WHERE ur.user_id = $1
- GROUP BY r.id, r.name, r.description
- `, [user.id]);
-
- const roles = userRoles.rows.map(r => r.name);
- const permissions = [...new Set(userRoles.rows.flatMap(r => r.permissions || []))];
-
- // Generate JWT token
- const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
- const token = jwt.sign(
- {
- userId: user.id,
- username: user.username,
- email: user.email,
- roles,
- permissions
- },
- jwtSecret,
- { expiresIn: '7d' }
- );
-
- // Store session
- const tokenHash = await bcrypt.hash(token, 10);
- const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
-
- await client.query(`
- INSERT INTO user_sessions (user_id, token_hash, expires_at, user_agent, ip_address)
- VALUES ($1, $2, $3, $4, $5)
- `, [
- user.id,
- tokenHash,
- expiresAt,
- req.headers['user-agent'] || null,
- req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || null
- ]);
-
- // Update last login
- await client.query(
- 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1',
- [user.id]
- );
-
- res.status(200).json({
- success: true,
- message: 'Login successful',
- user: {
- id: user.id,
- username: user.username,
- email: user.email,
- firstName: user.first_name,
- lastName: user.last_name,
- roles,
- permissions,
- lastLogin: user.last_login
- },
- token
- });
-
- } catch (error) {
- console.error('Login error:', error);
- res.status(500).json({
- error: 'Login failed',
- details: process.env.NODE_ENV === 'development' ? error.message : undefined
- });
- } finally {
- client.release();
- }
-}
\ No newline at end of file
diff --git a/pages/api/auth/register.js b/pages/api/auth/register.js
deleted file mode 100644
index 1b4f1af..0000000
--- a/pages/api/auth/register.js
+++ /dev/null
@@ -1,158 +0,0 @@
-const { Pool } = require('pg');
-const bcrypt = require('bcryptjs');
-const jwt = require('jsonwebtoken');
-
-const pool = new Pool({
- connectionString: process.env.DATABASE_URL,
- ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
-});
-
-export default async function handler(req, res) {
- if (req.method !== 'POST') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- // Check environment variables
- if (!process.env.DATABASE_URL) {
- return res.status(500).json({
- error: 'Database configuration missing',
- details: 'DATABASE_URL environment variable not set'
- });
- }
-
- if (!process.env.JWT_SECRET) {
- console.warn('JWT_SECRET not set, using fallback');
- }
-
- const client = await pool.connect();
-
- try {
- const { username, email, password, firstName, lastName } = req.body;
-
- // Validate input
- if (!username || !email || !password) {
- return res.status(400).json({
- error: 'Username, email, and password are required'
- });
- }
-
- if (password.length < 6) {
- return res.status(400).json({
- error: 'Password must be at least 6 characters long'
- });
- }
-
- // Check if users table exists
- const tableCheck = await client.query(`
- SELECT EXISTS (
- SELECT FROM information_schema.tables
- WHERE table_schema = 'public'
- AND table_name = 'users'
- );
- `);
-
- if (!tableCheck.rows[0].exists) {
- return res.status(500).json({
- error: 'Database not initialized',
- details: 'Please run the setup-auth endpoint first'
- });
- }
-
- // Check if user already exists
- const existingUser = await client.query(
- 'SELECT id FROM users WHERE username = $1 OR email = $2',
- [username, email]
- );
-
- if (existingUser.rows.length > 0) {
- return res.status(409).json({
- error: 'Username or email already exists'
- });
- }
-
- // Hash password
- const saltRounds = 12;
- const passwordHash = await bcrypt.hash(password, saltRounds);
-
- // Create user
- const userResult = await client.query(`
- INSERT INTO users (username, email, password_hash, first_name, last_name)
- VALUES ($1, $2, $3, $4, $5)
- RETURNING id, username, email, first_name, last_name, created_at
- `, [username, email, passwordHash, firstName || null, lastName || null]);
-
- const user = userResult.rows[0];
-
- // Assign default 'user' role
- const roleResult = await client.query(
- 'SELECT id FROM roles WHERE name = $1',
- ['user']
- );
-
- if (roleResult.rows.length > 0) {
- await client.query(
- 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)',
- [user.id, roleResult.rows[0].id]
- );
- }
-
- // Generate JWT token
- const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
- const token = jwt.sign(
- {
- userId: user.id,
- username: user.username,
- email: user.email
- },
- jwtSecret,
- { expiresIn: '7d' }
- );
-
- // Store session
- const tokenHash = await bcrypt.hash(token, 10);
- const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
-
- await client.query(`
- INSERT INTO user_sessions (user_id, token_hash, expires_at, user_agent, ip_address)
- VALUES ($1, $2, $3, $4, $5)
- `, [
- user.id,
- tokenHash,
- expiresAt,
- req.headers['user-agent'] || null,
- req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || null
- ]);
-
- // Get user roles for response
- const userRoles = await client.query(`
- SELECT r.name, r.description
- FROM roles r
- JOIN user_roles ur ON r.id = ur.role_id
- WHERE ur.user_id = $1
- `, [user.id]);
-
- res.status(201).json({
- success: true,
- message: 'User registered successfully',
- user: {
- id: user.id,
- username: user.username,
- email: user.email,
- firstName: user.first_name,
- lastName: user.last_name,
- roles: userRoles.rows.map(r => r.name),
- createdAt: user.created_at
- },
- token
- });
-
- } catch (error) {
- console.error('Registration error:', error);
- res.status(500).json({
- error: 'Registration failed',
- details: process.env.NODE_ENV === 'development' ? error.message : undefined
- });
- } finally {
- client.release();
- }
-}
\ No newline at end of file
diff --git a/pages/api/cards/index.js b/pages/api/cards/index.js
deleted file mode 100644
index 9b37701..0000000
--- a/pages/api/cards/index.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import { sql } from '@vercel/postgres';
-import { verifyToken } from '../auth-utils.js';
-
-// GET /api/cards - Search cards from database
-export default async function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- if (req.method !== 'GET' && req.method !== 'POST') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- const { q, search, game, page = '1', limit = '20' } = req.query;
- const query = q || search || '';
- const pageNum = parseInt(page);
- const limitNum = parseInt(limit);
- const offset = (pageNum - 1) * limitNum;
-
- console.log(`🔍 Searching cards: "${query}" game: "${game}" page: ${pageNum}`);
-
- // Build the SQL query
- let sqlQuery = `
- SELECT
- id,
- name,
- set_name,
- set_code,
- card_number,
- rarity,
- game,
- mana_cost,
- cmc,
- card_type,
- colors,
- oracle_text,
- power,
- toughness,
- image_url,
- stock_image_url,
- current_price,
- market_price,
- verified,
- created_at,
- updated_at
- FROM cards
- WHERE 1=1
- `;
-
- const params = [];
- let paramIndex = 1;
-
- // Add search filter
- if (query.trim()) {
- sqlQuery += ` AND (
- name ILIKE $${paramIndex} OR
- oracle_text ILIKE $${paramIndex} OR
- card_type ILIKE $${paramIndex} OR
- set_name ILIKE $${paramIndex}
- )`;
- params.push(`%${query}%`);
- paramIndex++;
- }
-
- // Add game filter
- if (game && game !== 'ALL') {
- sqlQuery += ` AND game = $${paramIndex}`;
- params.push(game);
- paramIndex++;
- }
-
- // Add ordering and pagination
- sqlQuery += ` ORDER BY name ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
- params.push(limitNum, offset);
-
- console.log(`📝 SQL Query: ${sqlQuery}`);
- console.log(`📝 Parameters:`, params);
-
- // Execute the query
- const result = await sql.query(sqlQuery, params);
-
- // Get total count for pagination
- let countQuery = `
- SELECT COUNT(*) as total
- FROM cards
- WHERE 1=1
- `;
-
- const countParams = [];
- let countParamIndex = 1;
-
- if (query.trim()) {
- countQuery += ` AND (
- name ILIKE $${countParamIndex} OR
- oracle_text ILIKE $${countParamIndex} OR
- card_type ILIKE $${countParamIndex} OR
- set_name ILIKE $${countParamIndex}
- )`;
- countParams.push(`%${query}%`);
- countParamIndex++;
- }
-
- if (game && game !== 'ALL') {
- countQuery += ` AND game = $${countParamIndex}`;
- countParams.push(game);
- countParamIndex++;
- }
-
- const countResult = await sql.query(countQuery, countParams);
- const total = parseInt(countResult.rows[0].total);
-
- // Format the response
- const cards = result.rows.map(card => ({
- id: card.id,
- name: card.name,
- setName: card.set_name,
- setCode: card.set_code,
- cardNumber: card.card_number,
- rarity: card.rarity,
- game: card.game,
- manaCost: card.mana_cost,
- cmc: card.cmc,
- cardType: card.card_type,
- colors: card.colors ? JSON.parse(card.colors) : [],
- oracleText: card.oracle_text,
- power: card.power,
- toughness: card.toughness,
- imageUrl: card.image_url,
- stockImageUrl: card.stock_image_url,
- currentPrice: card.current_price,
- marketPrice: card.market_price,
- verified: card.verified,
- createdAt: card.created_at,
- updatedAt: card.updated_at
- }));
-
- return res.status(200).json({
- success: true,
- cards,
- pagination: {
- page: pageNum,
- limit: limitNum,
- total,
- pages: Math.ceil(total / limitNum)
- }
- });
-
- } catch (error) {
- console.error('❌ Error in cards API:', error);
- return res.status(500).json({
- error: 'Failed to search cards',
- details: error.message
- });
- }
-}
\ No newline at end of file
diff --git a/pages/api/collections/index.js b/pages/api/collections/index.js
deleted file mode 100644
index 3eabbca..0000000
--- a/pages/api/collections/index.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import { sql } from '@vercel/postgres';
-import { verifyToken } from '../auth-utils.js';
-
-// GET /api/collections - Get user collections
-export default async function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- if (req.method !== 'GET' && req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'DELETE') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- // Temporarily bypass auth for testing
- // const token = req.headers.authorization?.replace('Bearer ', '');
- // const user = await verifyToken(token);
-
- // if (!user) {
- // return res.status(401).json({ error: 'Unauthorized' });
- // }
-
- const userId = 1; // Temporarily hardcoded for testing
-
- if (req.method === 'GET') {
- const { id } = req.query;
-
- if (id) {
- // Get specific collection
- const result = await sql.query(`
- SELECT
- c.id,
- c.name,
- c.description,
- c.is_public,
- c.created_at,
- c.updated_at,
- COUNT(cc.user_card_id) as card_count
- FROM collections c
- LEFT JOIN collection_cards cc ON c.id = cc.collection_id
- WHERE c.id = $1 AND c.user_id = $2
- GROUP BY c.id
- `, [id, userId]);
-
- if (result.rows.length === 0) {
- return res.status(404).json({ error: 'Collection not found' });
- }
-
- return res.status(200).json({
- success: true,
- collection: result.rows[0]
- });
- } else {
- // Get all collections
- const result = await sql.query(`
- SELECT
- c.id,
- c.name,
- c.description,
- c.is_public,
- c.created_at,
- c.updated_at,
- COUNT(cc.user_card_id) as card_count
- FROM collections c
- LEFT JOIN collection_cards cc ON c.id = cc.collection_id
- WHERE c.user_id = $1
- GROUP BY c.id
- ORDER BY c.created_at DESC
- `, [userId]);
-
- return res.status(200).json({
- success: true,
- collections: result.rows
- });
- }
- }
-
- if (req.method === 'POST') {
- const { name, description = '', isPublic = false } = req.body;
-
- if (!name) {
- return res.status(400).json({ error: 'Collection name is required' });
- }
-
- const result = await sql.query(`
- INSERT INTO collections (user_id, name, description, is_public)
- VALUES ($1, $2, $3, $4)
- RETURNING *
- `, [userId, name, description, isPublic]);
-
- return res.status(201).json({
- success: true,
- message: 'Collection created',
- collection: result.rows[0]
- });
- }
-
- if (req.method === 'PUT') {
- const { id, name, description, isPublic } = req.body;
-
- if (!id) {
- return res.status(400).json({ error: 'Collection ID is required' });
- }
-
- const result = await sql.query(`
- UPDATE collections
- SET name = COALESCE($1, name),
- description = COALESCE($2, description),
- is_public = COALESCE($3, is_public),
- updated_at = NOW()
- WHERE id = $4 AND user_id = $5
- RETURNING *
- `, [name, description, isPublic, id, userId]);
-
- if (result.rows.length === 0) {
- return res.status(404).json({ error: 'Collection not found' });
- }
-
- return res.status(200).json({
- success: true,
- message: 'Collection updated',
- collection: result.rows[0]
- });
- }
-
- if (req.method === 'DELETE') {
- const { id } = req.query;
-
- if (!id) {
- return res.status(400).json({ error: 'Collection ID is required' });
- }
-
- // Delete collection cards first
- await sql.query(`
- DELETE FROM collection_cards
- WHERE collection_id = $1
- `, [id]);
-
- // Delete the collection
- const result = await sql.query(`
- DELETE FROM collections
- WHERE id = $1 AND user_id = $2
- RETURNING *
- `, [id, userId]);
-
- if (result.rows.length === 0) {
- return res.status(404).json({ error: 'Collection not found' });
- }
-
- return res.status(200).json({
- success: true,
- message: 'Collection deleted'
- });
- }
-
- } catch (error) {
- console.error('❌ Error in collections API:', error);
- return res.status(500).json({
- error: 'Failed to process collections request',
- details: error.message
- });
- }
-}
\ No newline at end of file
diff --git a/pages/api/health.js b/pages/api/health.js
index 532841a..0892039 100644
--- a/pages/api/health.js
+++ b/pages/api/health.js
@@ -1,9 +1,7 @@
export default function handler(req, res) {
- // Minimal health check - no external dependencies
res.status(200).json({
status: 'ok',
- timestamp: new Date().toISOString(),
- method: req.method,
- url: req.url
+ message: 'Clean Next.js API is working!',
+ timestamp: new Date().toISOString()
});
}
\ No newline at end of file
diff --git a/pages/api/hello.js b/pages/api/hello.js
deleted file mode 100644
index 220745f..0000000
--- a/pages/api/hello.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function handler(req, res) {
- res.status(200).json({ message: 'Hello from Next.js API!' })
-}
\ No newline at end of file
diff --git a/pages/api/proxy/lorcana.js b/pages/api/proxy/lorcana.js
deleted file mode 100644
index 91c5677..0000000
--- a/pages/api/proxy/lorcana.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { NextResponse } from 'next/server';
-
-// Proxy for Lorcana APIs to handle CORS
-export async function GET(request) {
- try {
- const { searchParams } = new URL(request.url);
- const query = searchParams.get('q');
- const search = searchParams.get('search');
- const limit = searchParams.get('limit') || '20';
- const api = searchParams.get('api') || 'lorcana'; // 'lorcana' or 'lorcast'
-
- let url;
- let headers = {};
-
- if (api === 'lorcana') {
- // Lorcana API - using correct endpoint from docs
- if (search) {
- // Use the correct search parameter format for Lorcana API
- url = `https://api.lorcana-api.com/cards/fetch?search=name~${encodeURIComponent(search)}`;
- } else {
- url = `https://api.lorcana-api.com/cards/fetch?pagesize=${limit}`;
- }
- headers = {
- 'Accept': 'application/json',
- 'User-Agent': 'TCG-Vault/1.0'
- };
- } else {
- // Lorcast API - using correct endpoint from docs
- if (query) {
- url = `https://api.lorcast.com/v0/cards?q=${encodeURIComponent(query)}`;
- } else {
- url = `https://api.lorcast.com/v0/cards`;
- }
- headers = {
- 'Accept': 'application/json',
- 'User-Agent': 'TCG-Vault/1.0'
- };
- }
-
- console.log(`🔗 Proxying request to: ${url}`);
-
- const response = await fetch(url, { headers });
-
- if (!response.ok) {
- console.error(`❌ Proxy error: ${response.status} ${response.statusText}`);
- return NextResponse.json(
- { error: `External API error: ${response.status}` },
- { status: response.status }
- );
- }
-
- const data = await response.json();
- console.log(`✅ Proxy success: ${url}`);
-
- return NextResponse.json({
- success: true,
- data: data,
- source: api
- });
-
- } catch (error) {
- console.error('Proxy error:', error);
- return NextResponse.json(
- { error: 'Failed to fetch from external API' },
- { status: 500 }
- );
- }
-}
\ No newline at end of file
diff --git a/pages/api/setup-auth.js b/pages/api/setup-auth.js
deleted file mode 100644
index d17adae..0000000
--- a/pages/api/setup-auth.js
+++ /dev/null
@@ -1,172 +0,0 @@
-const { Pool } = require('pg');
-
-const pool = new Pool({
- connectionString: process.env.DATABASE_URL,
- ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
-});
-
-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 {
- console.log('Setting up user authentication schema...');
-
- // Check if users table already exists
- const tableCheck = await client.query(`
- SELECT EXISTS (
- SELECT FROM information_schema.tables
- WHERE table_schema = 'public'
- AND table_name = 'users'
- );
- `);
-
- if (tableCheck.rows[0].exists) {
- return res.status(200).json({
- success: true,
- message: 'User authentication schema already exists',
- already_setup: true
- });
- }
-
- // Create tables one by one
- await client.query(`
- CREATE TABLE IF NOT EXISTS users (
- id SERIAL PRIMARY KEY,
- username VARCHAR(50) UNIQUE NOT NULL,
- email VARCHAR(100) UNIQUE NOT NULL,
- password_hash VARCHAR(255) NOT NULL,
- first_name VARCHAR(50),
- last_name VARCHAR(50),
- avatar_url TEXT,
- is_active BOOLEAN DEFAULT true,
- email_verified BOOLEAN DEFAULT false,
- created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
- last_login TIMESTAMP WITH TIME ZONE
- );
- `);
-
- await client.query(`
- CREATE TABLE IF NOT EXISTS roles (
- id SERIAL PRIMARY KEY,
- name VARCHAR(50) UNIQUE NOT NULL,
- description TEXT,
- created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
- );
- `);
-
- await client.query(`
- CREATE TABLE IF NOT EXISTS user_roles (
- id SERIAL PRIMARY KEY,
- user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
- role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
- assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
- assigned_by INTEGER REFERENCES users(id),
- UNIQUE(user_id, role_id)
- );
- `);
-
- await client.query(`
- CREATE TABLE IF NOT EXISTS permissions (
- id SERIAL PRIMARY KEY,
- name VARCHAR(100) UNIQUE NOT NULL,
- description TEXT,
- resource VARCHAR(50),
- action VARCHAR(50),
- created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
- );
- `);
-
- await client.query(`
- CREATE TABLE IF NOT EXISTS role_permissions (
- id SERIAL PRIMARY KEY,
- role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
- permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
- UNIQUE(role_id, permission_id)
- );
- `);
-
- await client.query(`
- CREATE TABLE IF NOT EXISTS user_sessions (
- id SERIAL PRIMARY KEY,
- user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
- token_hash VARCHAR(255) NOT NULL,
- expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
- created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
- last_used TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
- user_agent TEXT,
- ip_address INET
- );
- `);
-
- // Insert default roles
- await client.query(`
- INSERT INTO roles (name, description) VALUES
- ('user', 'Standard user with basic permissions'),
- ('admin', 'Administrator with full system access')
- ON CONFLICT (name) DO NOTHING;
- `);
-
- // Insert basic permissions
- const permissions = [
- ['cards.read', 'View cards'],
- ['collections.manage', 'Manage collections'],
- ['decks.manage', 'Manage decks'],
- ['admin.access', 'Access admin panel']
- ];
-
- for (const [name, description] of permissions) {
- await client.query(`
- INSERT INTO permissions (name, description)
- VALUES ($1, $2)
- ON CONFLICT (name) DO NOTHING;
- `, [name, description]);
- }
-
- // Assign permissions to roles
- await client.query(`
- INSERT INTO role_permissions (role_id, permission_id)
- SELECT r.id, p.id
- FROM roles r, permissions p
- WHERE r.name = 'user'
- AND p.name IN ('cards.read', 'collections.manage', 'decks.manage')
- ON CONFLICT (role_id, permission_id) DO NOTHING;
- `);
-
- await client.query(`
- INSERT INTO role_permissions (role_id, permission_id)
- SELECT r.id, p.id
- FROM roles r, permissions p
- WHERE r.name = 'admin'
- ON CONFLICT (role_id, permission_id) DO NOTHING;
- `);
-
- // Create indexes
- await client.query(`
- CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
- CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
- CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
- `);
-
- console.log('✅ User authentication schema setup completed!');
-
- res.status(200).json({
- success: true,
- message: 'User authentication schema setup completed successfully'
- });
-
- } catch (error) {
- console.error('Schema setup failed:', error);
- res.status(500).json({
- success: false,
- error: 'Schema setup failed',
- details: error.message
- });
- } finally {
- client.release();
- }
-}
\ No newline at end of file
diff --git a/pages/api/test-basic.js b/pages/api/test-basic.js
deleted file mode 100644
index c444b88..0000000
--- a/pages/api/test-basic.js
+++ /dev/null
@@ -1,22 +0,0 @@
-export default function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- // Simple response without any external dependencies
- res.status(200).json({
- success: true,
- message: 'Basic API is working!',
- timestamp: new Date().toISOString(),
- method: req.method,
- url: req.url,
- headers: Object.keys(req.headers)
- });
-}
\ No newline at end of file
diff --git a/pages/api/test-force-deploy.js b/pages/api/test-force-deploy.js
deleted file mode 100644
index 9e6475a..0000000
--- a/pages/api/test-force-deploy.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default function handler(req, res) {
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- res.status(200).json({
- success: true,
- message: 'Force deployment test - this should trigger a fresh build!',
- timestamp: new Date().toISOString(),
- deployment: 'fresh'
- });
-}
\ No newline at end of file
diff --git a/pages/api/test-postgres-dynamic.js b/pages/api/test-postgres-dynamic.js
deleted file mode 100644
index bc41fae..0000000
--- a/pages/api/test-postgres-dynamic.js
+++ /dev/null
@@ -1,38 +0,0 @@
-export default async function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- if (req.method !== 'GET') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- // Dynamically import postgres to avoid module loading issues
- const { sql } = await import('@vercel/postgres');
-
- // Test database connection
- const result = await sql`SELECT NOW() as current_time, version() as postgres_version`;
-
- return res.status(200).json({
- success: true,
- message: 'PostgreSQL connection successful!',
- data: result.rows[0],
- timestamp: new Date().toISOString()
- });
- } catch (error) {
- console.error('❌ PostgreSQL connection error:', error);
- return res.status(500).json({
- error: 'PostgreSQL connection failed',
- details: error.message,
- stack: error.stack
- });
- }
-}
\ No newline at end of file
diff --git a/pages/api/test-postgres-require.js b/pages/api/test-postgres-require.js
deleted file mode 100644
index 35917e2..0000000
--- a/pages/api/test-postgres-require.js
+++ /dev/null
@@ -1,38 +0,0 @@
-export default async function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- if (req.method !== 'GET') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- // Use require instead of import
- const { sql } = require('@vercel/postgres');
-
- // Test database connection
- const result = await sql`SELECT NOW() as current_time, version() as postgres_version`;
-
- return res.status(200).json({
- success: true,
- message: 'PostgreSQL connection successful!',
- data: result.rows[0],
- timestamp: new Date().toISOString()
- });
- } catch (error) {
- console.error('❌ PostgreSQL connection error:', error);
- return res.status(500).json({
- error: 'PostgreSQL connection failed',
- details: error.message,
- stack: error.stack
- });
- }
-}
\ No newline at end of file
diff --git a/pages/api/test-postgres.js b/pages/api/test-postgres.js
deleted file mode 100644
index 241cb42..0000000
--- a/pages/api/test-postgres.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { sql } from '@vercel/postgres';
-
-export default async function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- if (req.method !== 'GET') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- // Test database connection
- const result = await sql`SELECT NOW() as current_time, version() as postgres_version`;
-
- return res.status(200).json({
- success: true,
- message: 'PostgreSQL connection successful!',
- data: result.rows[0],
- timestamp: new Date().toISOString()
- });
- } catch (error) {
- console.error('❌ PostgreSQL connection error:', error);
- return res.status(500).json({
- error: 'PostgreSQL connection failed',
- details: error.message,
- stack: error.stack
- });
- }
-}
\ No newline at end of file
diff --git a/pages/api/test-simple.js b/pages/api/test-simple.js
deleted file mode 100644
index a3eb19f..0000000
--- a/pages/api/test-simple.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export default function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- res.status(200).json({
- message: 'Simple API test successful!',
- timestamp: new Date().toISOString(),
- method: req.method,
- url: req.url
- });
-}
\ No newline at end of file
diff --git a/pages/api/test.js b/pages/api/test.js
deleted file mode 100644
index 45284a1..0000000
--- a/pages/api/test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { NextResponse } from 'next/server';
-
-export async function GET(request) {
- try {
- return NextResponse.json({
- success: true,
- message: 'Test API is working!',
- timestamp: new Date().toISOString()
- });
- } catch (error) {
- console.error('❌ Error in test API:', error);
- return NextResponse.json(
- { error: 'Test API failed', details: error.message },
- { status: 500 }
- );
- }
-}
-
-export async function POST(request) {
- try {
- const body = await request.json();
- return NextResponse.json({
- success: true,
- message: 'Test POST API is working!',
- receivedData: body,
- timestamp: new Date().toISOString()
- });
- } catch (error) {
- console.error('❌ Error in test POST API:', error);
- return NextResponse.json(
- { error: 'Test POST API failed', details: error.message },
- { status: 500 }
- );
- }
-}
\ No newline at end of file
diff --git a/pages/api/tsconfig.json b/pages/api/tsconfig.json
deleted file mode 100644
index 18603cf..0000000
--- a/pages/api/tsconfig.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "module": "CommonJS",
- "moduleResolution": "node",
- "esModuleInterop": true,
- "allowSyntheticDefaultImports": true,
- "strict": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "declaration": false,
- "outDir": "./dist"
- },
- "include": [
- "**/*.ts"
- ],
- "exclude": [
- "node_modules",
- "dist"
- ]
-}
\ No newline at end of file
diff --git a/pages/api/user-cards.js b/pages/api/user-cards.js
deleted file mode 100644
index a978148..0000000
--- a/pages/api/user-cards.js
+++ /dev/null
@@ -1,265 +0,0 @@
-import { sql } from '@vercel/postgres';
-import { verifyToken } from './auth-utils.js';
-
-// GET /api/user-cards - Get user's cards with optional filters
-export default async function handler(req, res) {
- // Set CORS headers
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-
- // Handle preflight requests
- if (req.method === 'OPTIONS') {
- res.status(200).end();
- return;
- }
-
- if (req.method !== 'GET' && req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'DELETE') {
- return res.status(405).json({ error: 'Method not allowed' });
- }
-
- try {
- // Temporarily bypass auth for testing
- // const token = req.headers.authorization?.replace('Bearer ', '');
- // const user = await verifyToken(token);
-
- // if (!user) {
- // return res.status(401).json({ error: 'Unauthorized' });
- // }
-
- const userId = 1; // Temporarily hardcoded for testing
-
- if (req.method === 'GET') {
- const { game, status, page = '1', limit = '20' } = req.query;
- const pageNum = parseInt(page);
- const limitNum = parseInt(limit);
- const offset = (pageNum - 1) * limitNum;
-
- let sqlQuery = `
- SELECT
- uc.id,
- uc.user_id,
- uc.card_id,
- uc.quantity,
- uc.status,
- uc.condition,
- uc.notes,
- uc.created_at,
- uc.updated_at,
- c.name,
- c.set_name,
- c.set_code,
- c.card_number,
- c.rarity,
- c.game,
- c.mana_cost,
- c.cmc,
- c.card_type,
- c.colors,
- c.oracle_text,
- c.power,
- c.toughness,
- c.image_url,
- c.stock_image_url,
- c.current_price,
- c.market_price,
- c.verified
- FROM user_cards uc
- JOIN cards c ON uc.card_id = c.id
- WHERE uc.user_id = $1
- `;
-
- const params = [userId];
- let paramIndex = 2;
-
- if (game && game !== 'ALL') {
- sqlQuery += ` AND c.game = $${paramIndex}`;
- params.push(game);
- paramIndex++;
- }
-
- if (status && status !== 'ALL') {
- sqlQuery += ` AND uc.status = $${paramIndex}`;
- params.push(status);
- paramIndex++;
- }
-
- sqlQuery += ` ORDER BY c.name ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
- params.push(limitNum, offset);
-
- const result = await sql.query(sqlQuery, params);
-
- // Get total count
- let countQuery = `
- SELECT COUNT(*) as total
- FROM user_cards uc
- JOIN cards c ON uc.card_id = c.id
- WHERE uc.user_id = $1
- `;
-
- const countParams = [userId];
- let countParamIndex = 2;
-
- if (game && game !== 'ALL') {
- countQuery += ` AND c.game = $${countParamIndex}`;
- countParams.push(game);
- countParamIndex++;
- }
-
- if (status && status !== 'ALL') {
- countQuery += ` AND uc.status = $${countParamIndex}`;
- countParams.push(status);
- countParamIndex++;
- }
-
- const countResult = await sql.query(countQuery, countParams);
- const total = parseInt(countResult.rows[0].total);
-
- const userCards = result.rows.map(row => ({
- id: row.id,
- userId: row.user_id,
- cardId: row.card_id,
- quantity: row.quantity,
- status: row.status,
- condition: row.condition,
- notes: row.notes,
- createdAt: row.created_at,
- updatedAt: row.updated_at,
- card: {
- id: row.card_id,
- name: row.name,
- setName: row.set_name,
- setCode: row.set_code,
- cardNumber: row.card_number,
- rarity: row.rarity,
- game: row.game,
- manaCost: row.mana_cost,
- cmc: row.cmc,
- cardType: row.card_type,
- colors: row.colors ? JSON.parse(row.colors) : [],
- oracleText: row.oracle_text,
- power: row.power,
- toughness: row.toughness,
- imageUrl: row.image_url,
- stockImageUrl: row.stock_image_url,
- currentPrice: row.current_price,
- marketPrice: row.market_price,
- verified: row.verified
- }
- }));
-
- return res.status(200).json({
- success: true,
- userCards,
- pagination: {
- page: pageNum,
- limit: limitNum,
- total,
- pages: Math.ceil(total / limitNum)
- }
- });
- }
-
- if (req.method === 'POST') {
- const { cardId, quantity = 1, status = 'OWNED', condition = 'NM', notes = '' } = req.body;
-
- if (!cardId) {
- return res.status(400).json({ error: 'Card ID is required' });
- }
-
- // Check if user already has this card
- const existingCard = await sql.query(`
- SELECT * FROM user_cards
- WHERE user_id = $1 AND card_id = $2
- `, [userId, cardId]);
-
- if (existingCard.rows.length > 0) {
- // Update existing card
- const result = await sql.query(`
- UPDATE user_cards
- SET quantity = $1, status = $2, condition = $3, notes = $4, updated_at = NOW()
- WHERE user_id = $5 AND card_id = $6
- RETURNING *
- `, [quantity, status, condition, notes, userId, cardId]);
-
- return res.status(200).json({
- success: true,
- message: 'Card updated',
- userCard: result.rows[0]
- });
- } else {
- // Add new card
- const result = await sql.query(`
- INSERT INTO user_cards (user_id, card_id, quantity, status, condition, notes)
- VALUES ($1, $2, $3, $4, $5, $6)
- RETURNING *
- `, [userId, cardId, quantity, status, condition, notes]);
-
- return res.status(201).json({
- success: true,
- message: 'Card added',
- userCard: result.rows[0]
- });
- }
- }
-
- if (req.method === 'PUT') {
- const { id, quantity, status, condition, notes } = req.body;
-
- if (!id) {
- return res.status(400).json({ error: 'User card ID is required' });
- }
-
- const result = await sql.query(`
- UPDATE user_cards
- SET quantity = COALESCE($1, quantity),
- status = COALESCE($2, status),
- condition = COALESCE($3, condition),
- notes = COALESCE($4, notes),
- updated_at = NOW()
- WHERE id = $5 AND user_id = $6
- RETURNING *
- `, [quantity, status, condition, notes, id, userId]);
-
- if (result.rows.length === 0) {
- return res.status(404).json({ error: 'User card not found' });
- }
-
- return res.status(200).json({
- success: true,
- message: 'Card updated',
- userCard: result.rows[0]
- });
- }
-
- if (req.method === 'DELETE') {
- const { id } = req.query;
-
- if (!id) {
- return res.status(400).json({ error: 'User card ID is required' });
- }
-
- const result = await sql.query(`
- DELETE FROM user_cards
- WHERE id = $1 AND user_id = $2
- RETURNING *
- `, [id, userId]);
-
- if (result.rows.length === 0) {
- return res.status(404).json({ error: 'User card not found' });
- }
-
- return res.status(200).json({
- success: true,
- message: 'Card removed'
- });
- }
-
- } catch (error) {
- console.error('❌ Error in user-cards API:', error);
- return res.status(500).json({
- error: 'Failed to process user cards request',
- details: error.message
- });
- }
-}
\ No newline at end of file
diff --git a/pages/api/user-cards/[id].js b/pages/api/user-cards/[id].js
deleted file mode 100644
index ab4b844..0000000
--- a/pages/api/user-cards/[id].js
+++ /dev/null
@@ -1,282 +0,0 @@
-import { NextResponse } from 'next/server';
-import { sql } from '@vercel/postgres';
-import { verifyToken } from '../auth-utils.js';
-
-// GET /api/user-cards/[id] - Get single user card
-export async function GET(request, { params }) {
- try {
- const token = request.headers.get('authorization')?.replace('Bearer ', '');
- const user = await verifyToken(token);
-
- if (!user) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const { id } = params;
-
- const result = await sql.query(
- `SELECT
- uc.id,
- uc.user_id,
- uc.card_id,
- uc.status,
- uc.quantity,
- uc.condition,
- uc.notes,
- uc.acquired_date,
- uc.acquired_price,
- uc.acquired_from,
- uc.created_at,
- uc.updated_at,
- c.name,
- c.set_name,
- c.set_code,
- c.card_number,
- c.rarity,
- c.game,
- c.card_type,
- c.mana_cost,
- c.cmc,
- c.colors,
- c.oracle_text,
- c.power,
- c.toughness,
- c.image_url,
- c.stock_image_url,
- c.current_price,
- c.market_price,
- c.verified,
- c.created_at as card_created_at,
- c.updated_at as card_updated_at
- FROM user_cards uc
- JOIN cards c ON uc.card_id = c.id
- WHERE uc.id = $1 AND uc.user_id = $2`,
- [id, user.id]
- );
-
- if (result.rows.length === 0) {
- return NextResponse.json(
- { error: 'Card not found or not owned by user' },
- { status: 404 }
- );
- }
-
- const row = result.rows[0];
- const userCard = {
- id: row.id,
- userId: row.user_id,
- cardId: row.card_id,
- status: row.status,
- quantity: row.quantity,
- condition: row.condition,
- notes: row.notes,
- acquiredDate: row.acquired_date,
- acquiredPrice: row.acquired_price,
- acquiredFrom: row.acquired_from,
- createdAt: row.created_at,
- updatedAt: row.updated_at,
- card: {
- id: row.card_id,
- name: row.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,
- verified: row.verified,
- createdAt: row.card_created_at,
- updatedAt: row.card_updated_at,
- }
- };
-
- return NextResponse.json({
- success: true,
- data: userCard
- });
-
- } catch (error) {
- console.error('Error fetching user card:', error);
- return NextResponse.json(
- { error: 'Failed to fetch user card' },
- { status: 500 }
- );
- }
-}
-
-// PUT /api/user-cards/[id] - Update user card
-export async function PUT(request, { params }) {
- try {
- const token = request.headers.get('authorization')?.replace('Bearer ', '');
- const user = await verifyToken(token);
-
- if (!user) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const { id } = params;
- const body = await request.json();
-
- // Check if user owns this card
- const ownershipResult = await sql.query(
- 'SELECT id FROM user_cards WHERE id = $1 AND user_id = $2',
- [id, user.id]
- );
-
- if (ownershipResult.rows.length === 0) {
- return NextResponse.json(
- { error: 'Card not found or not owned by user' },
- { status: 404 }
- );
- }
-
- // Build update query dynamically
- const updateFields = [];
- const updateValues = [];
- let paramIndex = 1;
-
- if (body.status !== undefined) {
- updateFields.push(`status = $${paramIndex}`);
- updateValues.push(body.status);
- paramIndex++;
- }
-
- if (body.quantity !== undefined) {
- updateFields.push(`quantity = $${paramIndex}`);
- updateValues.push(body.quantity);
- paramIndex++;
- }
-
- if (body.condition !== undefined) {
- updateFields.push(`condition = $${paramIndex}`);
- updateValues.push(body.condition);
- paramIndex++;
- }
-
- if (body.notes !== undefined) {
- updateFields.push(`notes = $${paramIndex}`);
- updateValues.push(body.notes);
- paramIndex++;
- }
-
- if (body.acquiredDate !== undefined) {
- updateFields.push(`acquired_date = $${paramIndex}`);
- updateValues.push(body.acquiredDate);
- paramIndex++;
- }
-
- if (body.acquiredPrice !== undefined) {
- updateFields.push(`acquired_price = $${paramIndex}`);
- updateValues.push(body.acquiredPrice);
- paramIndex++;
- }
-
- if (body.acquiredFrom !== undefined) {
- updateFields.push(`acquired_from = $${paramIndex}`);
- updateValues.push(body.acquiredFrom);
- paramIndex++;
- }
-
- if (updateFields.length === 0) {
- return NextResponse.json(
- { error: 'No fields to update' },
- { status: 400 }
- );
- }
-
- updateFields.push(`updated_at = NOW()`);
- updateValues.push(id);
-
- const query = `
- UPDATE user_cards
- SET ${updateFields.join(', ')}
- WHERE id = $${paramIndex}
- RETURNING *
- `;
-
- const result = await sql.query(query, updateValues);
- const userCard = result.rows[0];
-
- return NextResponse.json({
- success: true,
- data: {
- id: userCard.id,
- userId: userCard.user_id,
- cardId: userCard.card_id,
- status: userCard.status,
- quantity: userCard.quantity,
- condition: userCard.condition,
- notes: userCard.notes,
- acquiredDate: userCard.acquired_date,
- acquiredPrice: userCard.acquired_price,
- acquiredFrom: userCard.acquired_from,
- createdAt: userCard.created_at,
- updatedAt: userCard.updated_at,
- },
- message: 'Card updated successfully'
- });
-
- } catch (error) {
- console.error('Error updating user card:', error);
- return NextResponse.json(
- { error: 'Failed to update card' },
- { status: 500 }
- );
- }
-}
-
-// DELETE /api/user-cards/[id] - Delete user card
-export async function DELETE(request, { params }) {
- try {
- const token = request.headers.get('authorization')?.replace('Bearer ', '');
- const user = await verifyToken(token);
-
- if (!user) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
- }
-
- const { id } = params;
-
- // Check if user owns this card
- const ownershipResult = await sql.query(
- 'SELECT id FROM user_cards WHERE id = $1 AND user_id = $2',
- [id, user.id]
- );
-
- if (ownershipResult.rows.length === 0) {
- return NextResponse.json(
- { error: 'Card not found or not owned by user' },
- { status: 404 }
- );
- }
-
- // Delete from collections and decks first
- await sql.query('DELETE FROM collection_cards WHERE user_card_id = $1', [id]);
- await sql.query('DELETE FROM deck_cards WHERE user_card_id = $1', [id]);
-
- // Delete the user card
- await sql.query('DELETE FROM user_cards WHERE id = $1', [id]);
-
- return NextResponse.json({
- success: true,
- message: 'Card removed from collection'
- });
-
- } catch (error) {
- console.error('Error deleting user card:', error);
- return NextResponse.json(
- { error: 'Failed to delete card' },
- { status: 500 }
- );
- }
-}
\ No newline at end of file
diff --git a/pages/api/user/preferences.js b/pages/api/user/preferences.js
deleted file mode 100644
index 2a4ec01..0000000
--- a/pages/api/user/preferences.js
+++ /dev/null
@@ -1,219 +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,
-});
-
-// Verify JWT token and extract user info
-function verifyAuth(req) {
- const authHeader = req.headers.authorization;
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
- throw new Error('No authorization 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 or expired token');
- }
-}
-
-export default async function handler(req, res) {
- const client = await pool.connect();
-
- try {
- const user = verifyAuth(req);
-
- // Ensure ocr_settings column exists (auto-migration)
- try {
- await client.query(`
- ALTER TABLE user_preferences
- ADD COLUMN IF NOT EXISTS ocr_settings JSONB DEFAULT '{
- "preferred_service": "openai",
- "openai_api_key": "",
- "ollama_url": "http://localhost:11434",
- "auto_add_to_collection": false,
- "confidence_threshold": 80
- }'::jsonb;
- `);
- } catch (error) {
- // Column might already exist, ignore error
- console.log('OCR settings column may already exist:', error.message);
- }
-
- if (req.method === 'GET') {
- // Get user preferences
- const result = await client.query(`
- SELECT
- default_view,
- items_per_page,
- enable_animations,
- enable_ocr,
- theme,
- privacy_settings,
- ocr_settings,
- created_at,
- updated_at
- FROM user_preferences
- WHERE user_id = $1
- `, [user.userId]);
-
- if (result.rows.length === 0) {
- // Create default preferences if none exist
- const defaultPrefs = {
- default_view: 'card',
- items_per_page: 20,
- enable_animations: true,
- enable_ocr: true,
- theme: 'light',
- privacy_settings: { collections_public: false, decks_public: false },
- ocr_settings: {
- preferred_service: 'openai',
- openai_api_key: '',
- ollama_url: 'http://localhost:11434',
- auto_add_to_collection: false,
- confidence_threshold: 80
- }
- };
-
- await client.query(`
- INSERT INTO user_preferences (
- user_id, default_view, items_per_page, enable_animations,
- enable_ocr, theme, privacy_settings, ocr_settings
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
- `, [
- user.userId,
- defaultPrefs.default_view,
- defaultPrefs.items_per_page,
- defaultPrefs.enable_animations,
- defaultPrefs.enable_ocr,
- defaultPrefs.theme,
- JSON.stringify(defaultPrefs.privacy_settings),
- JSON.stringify(defaultPrefs.ocr_settings)
- ]);
-
- return res.status(200).json({
- success: true,
- preferences: defaultPrefs
- });
- }
-
- const preferences = result.rows[0];
- res.status(200).json({
- success: true,
- preferences: {
- defaultView: preferences.default_view,
- itemsPerPage: preferences.items_per_page,
- enableAnimations: preferences.enable_animations,
- enableOcr: preferences.enable_ocr,
- theme: preferences.theme,
- privacySettings: preferences.privacy_settings,
- ocrSettings: preferences.ocr_settings || {
- preferred_service: 'openai',
- openai_api_key: '',
- ollama_url: 'http://localhost:11434',
- auto_add_to_collection: false,
- confidence_threshold: 80
- },
- createdAt: preferences.created_at,
- updatedAt: preferences.updated_at
- }
- });
-
- } else if (req.method === 'PUT') {
- // Update user preferences
- const {
- defaultView,
- itemsPerPage,
- enableAnimations,
- enableOcr,
- theme,
- privacySettings,
- ocrSettings
- } = req.body;
-
- // Validate OCR settings if provided
- if (ocrSettings) {
- const allowedServices = ['openai', 'ollama'];
- if (ocrSettings.preferred_service && !allowedServices.includes(ocrSettings.preferred_service)) {
- return res.status(400).json({
- error: 'Invalid OCR service. Must be "openai" or "ollama"'
- });
- }
-
- if (ocrSettings.confidence_threshold && (ocrSettings.confidence_threshold < 0 || ocrSettings.confidence_threshold > 100)) {
- return res.status(400).json({
- error: 'Confidence threshold must be between 0 and 100'
- });
- }
- }
-
- // Update preferences (upsert)
- const result = await client.query(`
- INSERT INTO user_preferences (
- user_id, default_view, items_per_page, enable_animations,
- enable_ocr, theme, privacy_settings, ocr_settings, updated_at
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP)
- ON CONFLICT (user_id)
- DO UPDATE SET
- default_view = COALESCE($2, user_preferences.default_view),
- items_per_page = COALESCE($3, user_preferences.items_per_page),
- enable_animations = COALESCE($4, user_preferences.enable_animations),
- enable_ocr = COALESCE($5, user_preferences.enable_ocr),
- theme = COALESCE($6, user_preferences.theme),
- privacy_settings = COALESCE($7, user_preferences.privacy_settings),
- ocr_settings = COALESCE($8, user_preferences.ocr_settings),
- updated_at = CURRENT_TIMESTAMP
- RETURNING *
- `, [
- user.userId,
- defaultView,
- itemsPerPage,
- enableAnimations,
- enableOcr,
- theme,
- privacySettings ? JSON.stringify(privacySettings) : null,
- ocrSettings ? JSON.stringify(ocrSettings) : null
- ]);
-
- const preferences = result.rows[0];
- res.status(200).json({
- success: true,
- message: 'Preferences updated successfully',
- preferences: {
- defaultView: preferences.default_view,
- itemsPerPage: preferences.items_per_page,
- enableAnimations: preferences.enable_animations,
- enableOcr: preferences.enable_ocr,
- theme: preferences.theme,
- privacySettings: preferences.privacy_settings,
- ocrSettings: preferences.ocr_settings,
- updatedAt: preferences.updated_at
- }
- });
-
- } else {
- res.status(405).json({ error: 'Method not allowed' });
- }
-
- } catch (error) {
- console.error('User preferences error:', error);
-
- if (error.message.includes('authorization') || error.message.includes('token')) {
- res.status(401).json({ error: 'Unauthorized' });
- } else {
- res.status(500).json({
- error: 'Failed to manage preferences',
- details: process.env.NODE_ENV === 'development' ? error.message : undefined
- });
- }
- } finally {
- client.release();
- }
-}
\ No newline at end of file
diff --git a/pages/index.js b/pages/index.js
new file mode 100644
index 0000000..6ae1ec9
--- /dev/null
+++ b/pages/index.js
@@ -0,0 +1,14 @@
+export default function Home() {
+ return (
+
+
+
+ TCG Vault
+
+
+ Clean Next.js setup is working!
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/pages/index.tsx b/pages/index.tsx
deleted file mode 100644
index 030e02d..0000000
--- a/pages/index.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import dynamic from 'next/dynamic';
-
-const App = dynamic(() => import('../src/App'), {
- ssr: false,
-});
-
-export default function HomePage() {
- return ;
-}
\ No newline at end of file
diff --git a/public/.htaccess b/public/.htaccess
deleted file mode 100644
index 7a1ad42..0000000
--- a/public/.htaccess
+++ /dev/null
@@ -1,4 +0,0 @@
-Options -MultiViews
-RewriteEngine On
-RewriteCond %{REQUEST_FILENAME} !-f
-RewriteRule ^ index.html [QSA,L]
\ No newline at end of file
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index a11777c..0000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a..0000000
Binary files a/public/logo192.png and /dev/null differ
diff --git a/public/logo512.png b/public/logo512.png
deleted file mode 100644
index a4e47a6..0000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/public/manifest.json b/public/manifest.json
deleted file mode 100644
index 7e33646..0000000
--- a/public/manifest.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "short_name": "TCG Vault",
- "name": "TCG Vault - Trading Card Game Collection Manager",
- "description": "Mobile-first PWA for managing your trading card game collections, decks, and scanning cards with AI-powered OCR",
- "icons": [
- {
- "src": "favicon.ico",
- "sizes": "64x64 32x32 24x24 16x16",
- "type": "image/x-icon"
- },
- {
- "src": "logo192.png",
- "type": "image/png",
- "sizes": "192x192"
- },
- {
- "src": "logo512.png",
- "type": "image/png",
- "sizes": "512x512",
- "purpose": "any maskable"
- }
- ],
- "start_url": "/",
- "display": "standalone",
- "orientation": "portrait-primary",
- "theme_color": "#8b5cf6",
- "background_color": "#ffffff",
- "categories": ["entertainment", "lifestyle", "utilities"],
- "prefer_related_applications": false
-}
diff --git a/public/robots.txt b/public/robots.txt
deleted file mode 100644
index e9e57dc..0000000
--- a/public/robots.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# https://www.robotstxt.org/robotstxt.html
-User-agent: *
-Disallow:
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e05..0000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/src/App.test.tsx b/src/App.test.tsx
deleted file mode 100644
index 2a68616..0000000
--- a/src/App.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 86f5f61..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import React from 'react';
-import { useRouter } from 'next/router';
-import { Analytics } from '@vercel/analytics/react';
-import { SpeedInsights } from '@vercel/speed-insights/react';
-import { useAuth } from './contexts/AuthContext';
-import { ThemeProvider } from './contexts/ThemeContext';
-import ResponsiveLayout from './components/ResponsiveLayout';
-import Dashboard from './pages/Dashboard';
-import Collections from './pages/Collections';
-import Decks from './pages/Decks';
-import Cards from './pages/Cards';
-import Scanner from './pages/Scanner';
-import Settings from './pages/Settings';
-import LoginForm from './components/auth/LoginForm';
-import RegisterForm from './components/auth/RegisterForm';
-import AdminPanel from './components/admin/AdminPanel';
-
-// Protected Route Component
-const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const { user, isLoading } = useAuth();
- const router = useRouter();
-
- if (isLoading) {
- return (
-
-
-
-
Loading TCG Vault...
-
-
- );
- }
-
- if (!user) {
- router.push('/login');
- return null;
- }
-
- return <>{children}>;
-};
-
-// Public Route Component (redirect if authenticated)
-const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const { user, isLoading } = useAuth();
- const router = useRouter();
-
- if (isLoading) {
- return (
-
-
-
-
Loading TCG Vault...
-
-
- );
- }
-
- if (user) {
- router.push('/dashboard');
- return null;
- }
-
- return <>{children}>;
-};
-
-// Responsive Layout Component
-const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- return (
-
- {children}
-
- );
-};
-
-// Auth Layout Component for login/register
-const AuthLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- return (
-
- );
-};
-
-function App() {
- const router = useRouter();
- const { pathname } = router;
-
- return (
-
- {/* Public Routes */}
- {pathname === '/login' && (
-
-
-
-
-
- )}
-
- {pathname === '/register' && (
-
-
-
-
-
- )}
-
- {/* Protected Routes */}
- {pathname === '/dashboard' && (
-
-
-
-
-
- )}
-
- {pathname === '/collections' && (
-
-
-
-
-
- )}
-
- {pathname === '/decks' && (
-
-
-
-
-
- )}
-
- {pathname === '/cards' && (
-
-
-
-
-
- )}
-
- {pathname === '/scanner' && (
-
-
-
-
-
- )}
-
- {pathname === '/settings' && (
-
-
-
-
-
- )}
-
- {/* Admin Routes */}
- {pathname === '/admin' && (
-
-
-
-
-
- )}
-
- {/* Default redirect */}
- {pathname === '/' && (
-
-
-
-
-
- )}
-
- {/* 404 fallback */}
- {!['/login', '/register', '/dashboard', '/collections', '/decks', '/cards', '/scanner', '/settings', '/admin', '/'].includes(pathname) && (
-
-
-
-
Page Not Found
-
The page you're looking for doesn't exist.
-
-
-
- )}
-
-
-
-
- );
-}
-
-export default App;
diff --git a/src/components/CameraScanner.tsx b/src/components/CameraScanner.tsx
deleted file mode 100644
index 2c57bc4..0000000
--- a/src/components/CameraScanner.tsx
+++ /dev/null
@@ -1,312 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { aiCardOCR, ollamaCardOCR, type CardOCRResult } from '../services/aiOcr';
-
-interface CameraScannerProps {
- onCardScanned: (cardData: any) => void;
- onError: (error: string) => void;
-}
-
-interface ScanResult {
- text: string;
- confidence: number;
- cardName?: string;
- setName?: string;
-}
-
-const CameraScanner: React.FC = ({ onCardScanned, onError }) => {
- const [isStreaming, setIsStreaming] = useState(false);
- const [isProcessing, setIsProcessing] = useState(false);
- const [scanResult, setScanResult] = useState(null);
- const [capturedImage, setCapturedImage] = useState(null);
-
-
- const videoRef = useRef(null);
- const canvasRef = useRef(null);
- const streamRef = useRef(null);
-
- // Start camera stream
- const startCamera = async () => {
- try {
- const stream = await navigator.mediaDevices.getUserMedia({
- video: {
- facingMode: 'environment', // Use back camera on mobile
- width: { ideal: 1920 },
- height: { ideal: 1080 }
- }
- });
-
- if (videoRef.current) {
- videoRef.current.srcObject = stream;
- streamRef.current = stream;
-
- // Wait for video to be ready
- videoRef.current.onloadedmetadata = () => {
- videoRef.current?.play().then(() => {
- setIsStreaming(true);
- }).catch((err) => {
- onError(`Video playback failed: ${err.message}`);
- });
- };
-
- videoRef.current.onerror = (err) => {
- onError('Video element error occurred');
- };
- } else {
- onError('Video element not available');
- }
- } catch (err: any) {
- console.error('Camera access error:', err);
- onError(`Unable to access camera: ${err.message}`);
- }
- };
-
- // Stop camera stream
- const stopCamera = () => {
- if (streamRef.current) {
- streamRef.current.getTracks().forEach(track => track.stop());
- streamRef.current = null;
- }
- setIsStreaming(false);
- setCapturedImage(null);
- setScanResult(null);
- };
-
- // Capture image from video stream
- const captureImage = () => {
- if (!videoRef.current || !canvasRef.current) return;
-
- const video = videoRef.current;
- const canvas = canvasRef.current;
- const ctx = canvas.getContext('2d');
-
- // Set canvas dimensions to match video
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
-
- // Draw current video frame to canvas
- ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
-
- // Get image data URL
- const imageDataUrl = canvas.toDataURL('image/jpeg', 0.8);
- setCapturedImage(imageDataUrl);
-
- // Process with OCR
- processImage(imageDataUrl);
- };
-
- // Process image with AI OCR
- const processImage = async (imageData: string) => {
- setIsProcessing(true);
- setScanResult(null);
-
- try {
- // Try OpenAI Vision first, fallback to Ollama if available
- let ocrResult: CardOCRResult;
-
- try {
- console.log('🤖 Trying OpenAI Vision API...');
- ocrResult = await aiCardOCR.analyzeCard(imageData);
- console.log('✅ OpenAI Vision result:', ocrResult);
- } catch (openaiError) {
- console.log('❌ OpenAI failed, trying Ollama:', openaiError);
- try {
- ocrResult = await ollamaCardOCR.analyzeCard(imageData);
- console.log('✅ Ollama Vision result:', ocrResult);
- } catch (ollamaError) {
- console.error('❌ All AI OCR methods failed');
- throw new Error('AI OCR services unavailable. Please configure OpenAI API key or run Ollama locally.');
- }
- }
-
- const result: ScanResult = {
- text: ocrResult.rawText,
- confidence: ocrResult.confidence,
- cardName: ocrResult.cardName,
- setName: ocrResult.setName
- };
-
- setScanResult(result);
-
- // Send enhanced data to card matcher
- if (ocrResult.cardName) {
- onCardScanned({
- name: ocrResult.cardName,
- set: ocrResult.setName || ocrResult.setCode,
- setCode: ocrResult.setCode,
- game: ocrResult.game,
- cardType: ocrResult.cardType,
- rarity: ocrResult.rarity,
- hp: ocrResult.hp,
- attacks: ocrResult.attacks,
- abilities: ocrResult.abilities,
- ocrText: ocrResult.rawText,
- confidence: ocrResult.confidence
- });
- }
-
- } catch (error: any) {
- console.error('AI OCR processing error:', error);
- onError(`AI OCR failed: ${error.message}`);
- } finally {
- setIsProcessing(false);
- }
- };
-
-
-
- // Cleanup on unmount
- useEffect(() => {
- return () => {
- stopCamera();
- };
- }, []);
-
- return (
-
- {/* Camera Controls */}
-
- {!isStreaming ? (
-
- ) : (
- <>
-
-
- >
- )}
-
-
- {/* Camera Preview - Always render video element */}
-
-
-
- {/* Show overlay only when streaming */}
- {isStreaming && (
-
-
-
-
Position card within this area
-
Ensure good lighting and focus
-
-
-
- )}
-
- {/* Show placeholder when not streaming */}
- {!isStreaming && (
-
-
-
📹
-
Camera Preview
-
Click "Start Camera" to begin
-
-
- )}
-
-
- {/* Hidden canvas for image capture */}
-
-
- {/* Processing Status */}
- {isProcessing && (
-
-
-
-
-
Processing image...
-
Extracting card information
-
-
-
- )}
-
- {/* Captured Image Preview */}
- {capturedImage && !isProcessing && (
-
-
Captured Image
-

-
- )}
-
- {/* OCR Results */}
- {scanResult && (
-
-
Scan Results
-
- {scanResult.cardName ? (
-
-
-
✅
-
-
Card Found: {scanResult.cardName}
- {scanResult.setName && (
-
Set: {scanResult.setName}
- )}
-
-
-
- Confidence: {Math.round(scanResult.confidence)}%
-
-
- ) : (
-
-
-
⚠️
-
Card not recognized
-
-
- View OCR text
-
- {scanResult.text}
-
-
-
- )}
-
- )}
-
- {/* Instructions */}
-
-
📋 Scanning Tips
-
- - • Ensure good lighting and avoid shadows
- - • Keep the card flat and in focus
- - • Position the card name clearly in view
- - • Avoid glare and reflections
- - • Works best with English cards
-
-
-
-
-
- );
-};
-
-export default CameraScanner;
\ No newline at end of file
diff --git a/src/components/CardImageDisplay.tsx b/src/components/CardImageDisplay.tsx
deleted file mode 100644
index 904e538..0000000
--- a/src/components/CardImageDisplay.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import React, { useState } from 'react';
-import { use3DTilt } from '../hooks/use3DTilt';
-
-interface CardImageDisplayProps {
- card: {
- id: string;
- name: string;
- game: string;
- stock_image_url?: string;
- image_url?: string;
- rarity: string;
- };
- userImages?: string[];
- size?: 'small' | 'medium' | 'large';
- showUserPhotos?: boolean;
- className?: string;
-}
-
-const CardImageDisplay: React.FC = ({
- card,
- userImages = [],
- size = 'medium',
- showUserPhotos: initialShowUserPhotos = false,
- className = ''
-}) => {
- const [imageError, setImageError] = useState(false);
- const [currentUserImageIndex, setCurrentUserImageIndex] = useState(0);
- const [showUserPhotos, setShowUserPhotos] = useState(initialShowUserPhotos);
-
- // 3D Tilt effect (no scaling - main container handles expansion)
- const { ref: tiltRef, tiltStyles } = use3DTilt({
- maxTilt: size === 'large' ? 15 : 10,
- scale: 1.0, // No image scaling - main container expands
- speed: 400,
- easing: 'cubic-bezier(0.23, 1, 0.320, 1)'
- });
-
- // Determine which image to show
- const getDisplayImage = () => {
- if (showUserPhotos && userImages.length > 0) {
- return userImages[currentUserImageIndex];
- }
- return card.stock_image_url || card.image_url;
- };
-
- // Determine if card should have foil effects
- const isFoilCard = () => {
- const foilRarities = ['super rare', 'legendary', 'mythic'];
- return foilRarities.includes(card.rarity.toLowerCase());
- };
-
- // Get rarity border class
- const getRarityBorderClass = () => {
- const rarity = card.rarity.toLowerCase().replace(/\s+/g, '');
- return `rarity-border-${rarity}`;
- };
-
- // Size classes
- const sizeClasses = {
- small: 'w-16 h-22',
- medium: 'w-24 h-32',
- large: 'w-48 h-64'
- };
-
- // Placeholder based on game
- const getPlaceholder = () => {
- const gameColors = {
- MTG: 'from-orange-400 to-red-500',
- POKEMON: 'from-yellow-400 to-red-500',
- LORCANA: 'from-purple-400 to-pink-500'
- };
-
- const gradientClass = gameColors[card.game as keyof typeof gameColors] || 'from-gray-400 to-gray-600';
- const rarityBorderClass = getRarityBorderClass();
- const foilClass = isFoilCard() ? 'foil-rainbow' : '';
-
- return (
-
-
🃏
-
- {card.name.split(' ').slice(0, 2).join(' ')}
-
-
- {card.game}
-
-
- );
- };
-
- const displayImage = getDisplayImage();
-
- if (!displayImage || imageError) {
- return getPlaceholder();
- }
-
- const rarityBorderClass = getRarityBorderClass();
- const foilClass = isFoilCard() ? 'foil-card' : '';
- const legendaryHoloClass = card.rarity.toLowerCase() === 'legendary' || card.rarity.toLowerCase() === 'mythic' ? 'holographic' : '';
-
- return (
-
-
-

setImageError(true)}
- />
-
-
- {/* User photo indicator */}
- {showUserPhotos && userImages.length > 0 && (
-
- 📸 {currentUserImageIndex + 1}/{userImages.length}
-
- )}
-
- {/* Stock/User toggle */}
- {userImages.length > 0 && (
-
-
-
- )}
-
- {/* User photo navigation */}
- {showUserPhotos && userImages.length > 1 && (
-
-
-
-
- )}
-
- {/* Image type indicator */}
-
- {showUserPhotos ? (
-
- 👤
-
- ) : (
-
- ✓
-
- )}
-
-
- );
-};
-
-export default CardImageDisplay;
\ No newline at end of file
diff --git a/src/components/GlowingCard.tsx b/src/components/GlowingCard.tsx
deleted file mode 100644
index 22b77df..0000000
--- a/src/components/GlowingCard.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-
-interface GlowingCardProps {
- children: React.ReactNode;
- rarity: string;
- className?: string;
-}
-
-const GlowingCard: React.FC = ({
- children,
- rarity,
- className = ''
-}) => {
- // No mouse glow - just static container
-
- // Get rarity border class
- const getRarityBorderClass = () => {
- const rarityName = rarity.toLowerCase().replace(/\s+/g, '');
- return `rarity-border-${rarityName}`;
- };
-
- // Get optimized animation class based on rarity
- const getAnimationClass = () => {
- switch (rarity.toLowerCase()) {
- case 'mythic':
- return 'mythic-expansion premium-card-animation';
- case 'legendary':
- return 'premium-card-animation';
- case 'super rare':
- return 'premium-card-animation'; // Optimized performance version
- default:
- return 'card-expansion-organic';
- }
- };
-
- return (
-
- {children}
-
- );
-};
-
-export default GlowingCard;
\ No newline at end of file
diff --git a/src/components/MobileNavbar.tsx b/src/components/MobileNavbar.tsx
deleted file mode 100644
index 15739ec..0000000
--- a/src/components/MobileNavbar.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-import React, { useState } from 'react';
-import { Link, useLocation, useNavigate } from 'react-router-dom';
-import { useAuth } from '../contexts/AuthContext';
-import { useTheme } from '../contexts/ThemeContext';
-
-const MobileNavbar: React.FC = () => {
- const { user, logout, isAdmin } = useAuth();
- const { toggleTheme, effectiveTheme } = useTheme();
- const location = useLocation();
- const navigate = useNavigate();
- const [showUserMenu, setShowUserMenu] = useState(false);
-
- const handleLogout = () => {
- logout();
- setShowUserMenu(false);
- navigate('/login');
- };
-
- const isActive = (path: string) => location.pathname === path;
-
- return (
- <>
- {/* Top Header */}
-
-
-
-
-
-
TCG Vault
-
- Hi, {user?.firstName || user?.username}
-
-
-
-
-
- {/* Theme Toggle */}
-
-
- {/* User Avatar */}
-
-
-
-
-
- {/* Bottom Navigation */}
-
-
- {/* User Menu Modal */}
- {showUserMenu && (
- setShowUserMenu(false)}>
-
-
-
-
-
- {user?.firstName?.[0] || user?.username?.[0]?.toUpperCase() || 'U'}
-
-
-
- {user?.firstName && user?.lastName
- ? `${user.firstName} ${user.lastName}`
- : user?.username}
-
-
{user?.email}
-
- {user?.roles.map((role) => (
-
- {role}
-
- ))}
-
-
-
-
-
-
setShowUserMenu(false)}
- className="flex items-center space-x-3 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
- >
-
-
Decks
-
-
-
setShowUserMenu(false)}
- className="flex items-center space-x-3 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
- >
-
-
Settings
-
-
- {isAdmin() && (
-
setShowUserMenu(false)}
- className="flex items-center space-x-3 p-3 rounded-xl hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors"
- >
-
-
Admin Panel
-
- )}
-
-
-
-
-
- )}
- >
- );
-};
-
-export default MobileNavbar;
\ No newline at end of file
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
deleted file mode 100644
index a2c7327..0000000
--- a/src/components/Navbar.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import React, { useState } from 'react';
-import { Link, useLocation, useNavigate } from 'react-router-dom';
-import { useAuth } from '../contexts/AuthContext';
-
-const Navbar: React.FC = () => {
- const { user, logout, isAdmin } = useAuth();
- const location = useLocation();
- const navigate = useNavigate();
- const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
-
- const handleLogout = () => {
- logout();
- setIsUserMenuOpen(false);
- navigate('/login');
- };
-
- const navigation = [
- { name: 'Dashboard', href: '/dashboard', icon: '📊' },
- { name: 'Cards', href: '/cards', icon: '🃏' },
- { name: 'Collections', href: '/collections', icon: '📚' },
- { name: 'Decks', href: '/decks', icon: '🎯' },
- { name: 'Scanner', href: '/scanner', icon: '📷' },
- ];
-
- const isActive = (path: string) => location.pathname === path;
-
- return (
-