From 7be41b44902a82c4ab38f8908ff364439b6daaf7 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 23 Jul 2025 09:32:31 -0500 Subject: [PATCH] Clean slate: Remove React traces and create pure Next.js setup --- next.config.js | 21 - package-lock.json | 27 +- package.json | 52 +- pages/_app.js | 5 + pages/_app.tsx | 24 - pages/api/admin/index.js | 409 -------- pages/api/auth-utils.js | 53 -- pages/api/auth/login.js | 125 --- pages/api/auth/register.js | 158 ---- pages/api/cards/index.js | 163 ---- pages/api/collections/index.js | 170 ---- pages/api/health.js | 6 +- pages/api/hello.js | 3 - pages/api/proxy/lorcana.js | 68 -- pages/api/setup-auth.js | 172 ---- pages/api/test-basic.js | 22 - pages/api/test-force-deploy.js | 17 - pages/api/test-postgres-dynamic.js | 38 - pages/api/test-postgres-require.js | 38 - pages/api/test-postgres.js | 37 - pages/api/test-simple.js | 19 - pages/api/test.js | 35 - pages/api/tsconfig.json | 22 - pages/api/user-cards.js | 265 ------ pages/api/user-cards/[id].js | 282 ------ pages/api/user/preferences.js | 219 ----- pages/index.js | 14 + pages/index.tsx | 10 - public/.htaccess | 4 - public/favicon.ico | Bin 3870 -> 0 bytes public/logo192.png | Bin 5347 -> 0 bytes public/logo512.png | Bin 9664 -> 0 bytes public/manifest.json | 30 - public/robots.txt | 3 - src/App.css | 38 - src/App.test.tsx | 9 - src/App.tsx | 211 ----- src/components/CameraScanner.tsx | 312 ------- src/components/CardImageDisplay.tsx | 175 ---- src/components/GlowingCard.tsx | 45 - src/components/MobileNavbar.tsx | 266 ------ src/components/Navbar.tsx | 212 ----- src/components/OCRSettings.tsx | 217 ----- src/components/ResponsiveLayout.tsx | 520 ----------- src/components/admin/AdminPanel.tsx | 126 --- src/components/admin/CardLoader.tsx | 186 ---- src/components/admin/CardManagement.tsx | 41 - src/components/admin/SystemStats.tsx | 182 ---- src/components/admin/UserManagement.tsx | 329 ------- src/components/auth/LoginForm.tsx | 154 ---- src/components/auth/RegisterForm.tsx | 249 ----- src/components/cards/CardDatabaseBrowser.tsx | 607 ------------ src/components/cards/CardManager.tsx | 365 -------- src/components/cards/CardSearch.tsx | 279 ------ .../collections/CollectionManager.tsx | 356 ------- src/components/decks/DeckManager.tsx | 408 -------- src/components/scanner/AutoScanningCamera.tsx | 510 ---------- src/components/scanner/BulkOperations.tsx | 277 ------ src/components/scanner/CardQueue.tsx | 224 ----- src/components/scanner/ScanModeSelector.tsx | 125 --- src/components/scanner/ScannerWizard.tsx | 475 ---------- src/components/scanner/ScanningToast.tsx | 86 -- src/components/tags/TagManager.tsx | 222 ----- src/config/api.ts | 26 - src/contexts/AuthContext.tsx | 158 ---- src/contexts/ThemeContext.tsx | 105 --- src/data/cards.json | 502 ---------- src/hooks/use3DTilt.ts | 113 --- src/hooks/useMouseGlow.ts | 121 --- src/index.css | 77 -- src/index.tsx | 32 - src/logo.svg | 1 - src/pages/Cards.tsx | 474 ---------- src/pages/Collections.tsx | 320 ------- src/pages/Dashboard.tsx | 182 ---- src/pages/Decks.tsx | 372 -------- src/pages/Login.tsx | 192 ---- src/pages/Scanner.tsx | 8 - src/pages/Settings.tsx | 496 ---------- src/react-app-env.d.ts | 1 - src/reportWebVitals.ts | 15 - src/services/aiOcr.ts | 300 ------ src/services/api.ts | 110 --- src/services/autoTagger.ts | 305 ------ src/services/cardDataSources.ts | 872 ------------------ src/services/cardMatcher.ts | 212 ----- src/services/tcgApi.ts | 410 -------- src/setupTests.ts | 5 - src/styles/cardEffects.css | 345 ------- src/types/index.ts | 295 ------ styles/globals.css | 3 + tailwind.config.js | 84 +- tsconfig.json | 35 +- 93 files changed, 58 insertions(+), 15830 deletions(-) create mode 100644 pages/_app.js delete mode 100644 pages/_app.tsx delete mode 100644 pages/api/admin/index.js delete mode 100644 pages/api/auth-utils.js delete mode 100644 pages/api/auth/login.js delete mode 100644 pages/api/auth/register.js delete mode 100644 pages/api/cards/index.js delete mode 100644 pages/api/collections/index.js delete mode 100644 pages/api/hello.js delete mode 100644 pages/api/proxy/lorcana.js delete mode 100644 pages/api/setup-auth.js delete mode 100644 pages/api/test-basic.js delete mode 100644 pages/api/test-force-deploy.js delete mode 100644 pages/api/test-postgres-dynamic.js delete mode 100644 pages/api/test-postgres-require.js delete mode 100644 pages/api/test-postgres.js delete mode 100644 pages/api/test-simple.js delete mode 100644 pages/api/test.js delete mode 100644 pages/api/tsconfig.json delete mode 100644 pages/api/user-cards.js delete mode 100644 pages/api/user-cards/[id].js delete mode 100644 pages/api/user/preferences.js create mode 100644 pages/index.js delete mode 100644 pages/index.tsx delete mode 100644 public/.htaccess delete mode 100644 public/favicon.ico delete mode 100644 public/logo192.png delete mode 100644 public/logo512.png delete mode 100644 public/manifest.json delete mode 100644 public/robots.txt delete mode 100644 src/App.css delete mode 100644 src/App.test.tsx delete mode 100644 src/App.tsx delete mode 100644 src/components/CameraScanner.tsx delete mode 100644 src/components/CardImageDisplay.tsx delete mode 100644 src/components/GlowingCard.tsx delete mode 100644 src/components/MobileNavbar.tsx delete mode 100644 src/components/Navbar.tsx delete mode 100644 src/components/OCRSettings.tsx delete mode 100644 src/components/ResponsiveLayout.tsx delete mode 100644 src/components/admin/AdminPanel.tsx delete mode 100644 src/components/admin/CardLoader.tsx delete mode 100644 src/components/admin/CardManagement.tsx delete mode 100644 src/components/admin/SystemStats.tsx delete mode 100644 src/components/admin/UserManagement.tsx delete mode 100644 src/components/auth/LoginForm.tsx delete mode 100644 src/components/auth/RegisterForm.tsx delete mode 100644 src/components/cards/CardDatabaseBrowser.tsx delete mode 100644 src/components/cards/CardManager.tsx delete mode 100644 src/components/cards/CardSearch.tsx delete mode 100644 src/components/collections/CollectionManager.tsx delete mode 100644 src/components/decks/DeckManager.tsx delete mode 100644 src/components/scanner/AutoScanningCamera.tsx delete mode 100644 src/components/scanner/BulkOperations.tsx delete mode 100644 src/components/scanner/CardQueue.tsx delete mode 100644 src/components/scanner/ScanModeSelector.tsx delete mode 100644 src/components/scanner/ScannerWizard.tsx delete mode 100644 src/components/scanner/ScanningToast.tsx delete mode 100644 src/components/tags/TagManager.tsx delete mode 100644 src/config/api.ts delete mode 100644 src/contexts/AuthContext.tsx delete mode 100644 src/contexts/ThemeContext.tsx delete mode 100644 src/data/cards.json delete mode 100644 src/hooks/use3DTilt.ts delete mode 100644 src/hooks/useMouseGlow.ts delete mode 100644 src/index.css delete mode 100644 src/index.tsx delete mode 100644 src/logo.svg delete mode 100644 src/pages/Cards.tsx delete mode 100644 src/pages/Collections.tsx delete mode 100644 src/pages/Dashboard.tsx delete mode 100644 src/pages/Decks.tsx delete mode 100644 src/pages/Login.tsx delete mode 100644 src/pages/Scanner.tsx delete mode 100644 src/pages/Settings.tsx delete mode 100644 src/react-app-env.d.ts delete mode 100644 src/reportWebVitals.ts delete mode 100644 src/services/aiOcr.ts delete mode 100644 src/services/api.ts delete mode 100644 src/services/autoTagger.ts delete mode 100644 src/services/cardDataSources.ts delete mode 100644 src/services/cardMatcher.ts delete mode 100644 src/services/tcgApi.ts delete mode 100644 src/setupTests.ts delete mode 100644 src/styles/cardEffects.css delete mode 100644 src/types/index.ts create mode 100644 styles/globals.css 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 a11777cc471a4344702741ab1c8a588998b1311a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a3796c0e0a64c3d858ca038bd4570465d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN 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 ( -
-
- {children} -
-
- ); -}; - -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 */} -
-
- - {/* Hidden canvas for image capture */} - - - {/* Processing Status */} - {isProcessing && ( -
-
-
-
-
Processing image...
-
Extracting card information
-
-
-
- )} - - {/* Captured Image Preview */} - {capturedImage && !isProcessing && ( -
-
Captured Image
- Captured card -
- )} - - {/* 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 ( -
-
- {card.name} 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 ( - - ); -}; - -export default Navbar; \ No newline at end of file diff --git a/src/components/OCRSettings.tsx b/src/components/OCRSettings.tsx deleted file mode 100644 index 97fd667..0000000 --- a/src/components/OCRSettings.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { aiCardOCR, ollamaCardOCR } from '../services/aiOcr'; - -interface OCRSettingsProps { - onClose: () => void; -} - -const OCRSettings: React.FC = ({ onClose }) => { - const [openaiKey, setOpenaiKey] = useState(''); - const [ollamaUrl, setOllamaUrl] = useState('http://localhost:11434'); - const [selectedService, setSelectedService] = useState<'openai' | 'ollama'>('openai'); - const [isTestingOpenAI, setIsTestingOpenAI] = useState(false); - const [isTestingOllama, setIsTestingOllama] = useState(false); - const [testResults, setTestResults] = useState<{ - openai?: string; - ollama?: string; - }>({}); - - useEffect(() => { - // Load saved settings - const savedKey = localStorage.getItem('openai_api_key'); - const savedUrl = localStorage.getItem('ollama_url'); - const savedService = localStorage.getItem('preferred_ocr_service') as 'openai' | 'ollama'; - - if (savedKey) setOpenaiKey(savedKey); - if (savedUrl) setOllamaUrl(savedUrl); - if (savedService) setSelectedService(savedService); - }, []); - - const saveSettings = () => { - if (openaiKey) { - localStorage.setItem('openai_api_key', openaiKey); - aiCardOCR.setApiKey(openaiKey); - } - localStorage.setItem('ollama_url', ollamaUrl); - localStorage.setItem('preferred_ocr_service', selectedService); - onClose(); - }; - - const testOpenAI = async () => { - if (!openaiKey) { - setTestResults(prev => ({ ...prev, openai: '❌ API key required' })); - return; - } - - setIsTestingOpenAI(true); - try { - // Test with a simple request - const response = await fetch('https://api.openai.com/v1/models', { - headers: { - 'Authorization': `Bearer ${openaiKey}`, - }, - }); - - if (response.ok) { - setTestResults(prev => ({ ...prev, openai: '✅ API key valid' })); - } else { - setTestResults(prev => ({ ...prev, openai: `❌ API error: ${response.status}` })); - } - } catch (error: any) { - setTestResults(prev => ({ ...prev, openai: `❌ Connection failed: ${error.message}` })); - } finally { - setIsTestingOpenAI(false); - } - }; - - const testOllama = async () => { - setIsTestingOllama(true); - try { - const response = await fetch(`${ollamaUrl}/api/tags`); - if (response.ok) { - const data = await response.json(); - const hasVisionModel = data.models?.some((model: any) => - model.name.includes('llava') || model.name.includes('vision') - ); - - if (hasVisionModel) { - setTestResults(prev => ({ ...prev, ollama: '✅ Ollama with vision models available' })); - } else { - setTestResults(prev => ({ ...prev, ollama: '⚠️ Ollama running but no vision models found' })); - } - } else { - setTestResults(prev => ({ ...prev, ollama: `❌ Ollama error: ${response.status}` })); - } - } catch (error: any) { - setTestResults(prev => ({ ...prev, ollama: `❌ Cannot reach Ollama: ${error.message}` })); - } finally { - setIsTestingOllama(false); - } - }; - - return ( -
- ); -}; - -export default OCRSettings; \ No newline at end of file diff --git a/src/components/ResponsiveLayout.tsx b/src/components/ResponsiveLayout.tsx deleted file mode 100644 index bd63799..0000000 --- a/src/components/ResponsiveLayout.tsx +++ /dev/null @@ -1,520 +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 ResponsiveLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { user, logout, isAdmin } = useAuth(); - const { toggleTheme, effectiveTheme } = useTheme(); - const location = useLocation(); - const navigate = useNavigate(); - const [showUserMenu, setShowUserMenu] = useState(false); - const [sidebarOpen, setSidebarOpen] = useState(false); - - const handleLogout = () => { - logout(); - setShowUserMenu(false); - setSidebarOpen(false); - navigate('/login'); - }; - - const isActive = (path: string) => location.pathname === path; - - const navigation = [ - { name: 'Dashboard', href: '/dashboard', icon: '📊', mobileIcon: '🏠' }, - { name: 'Cards', href: '/cards', icon: '🃏', mobileIcon: '🃏' }, - { name: 'Collections', href: '/collections', icon: '📚', mobileIcon: '📚' }, - { name: 'Decks', href: '/decks', icon: '🎯', mobileIcon: '🎯' }, - { name: 'Scanner', href: '/scanner', icon: '📷', mobileIcon: '📷' }, - ]; - - return ( -
- {/* Desktop Layout */} -
- {/* Sidebar */} -
- {/* Logo */} -
- -
- - - -
-
-

TCG Vault

-

- Hi, {user?.firstName || user?.username} -

-
- -
- - {/* Navigation */} - - - {/* User Section */} -
-
-
- {user?.firstName?.[0] || user?.username?.[0]?.toUpperCase() || 'U'} -
-
-

- {user?.firstName && user?.lastName - ? `${user.firstName} ${user.lastName}` - : user?.username} -

-

{user?.email}

-
- -
- - {/* User Menu Dropdown */} - {showUserMenu && ( -
-
- setShowUserMenu(false)} - className="flex items-center space-x-3 px-4 py-2 text-sm text-surface-700 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-700" - > - ⚙️ - Settings - - - {isAdmin() && ( - setShowUserMenu(false)} - className="flex items-center space-x-3 px-4 py-2 text-sm text-yellow-700 dark:text-yellow-300 hover:bg-yellow-50 dark:hover:bg-yellow-900/20" - > - 👑 - Admin Panel - - )} - -
- - -
-
- )} -
-
- - {/* Main Content */} -
- {/* Top Header */} -
-
-
-

- {navigation.find(item => isActive(item.href))?.name || 'TCG Vault'} -

- {isAdmin() && ( - - ⚡ Admin - - )} -
- -
- {/* Theme Toggle */} - -
-
-
- - {/* Page Content */} -
-
- {children} -
-
-
-
- - {/* Mobile Layout */} -
- {/* Top Header */} -
-
-
- -
- - - -
-
-

TCG Vault

-

- Hi, {user?.firstName || user?.username} -

-
-
- -
- {/* Theme Toggle */} - - - {/* User Avatar */} - -
-
-
- - {/* Mobile Sidebar */} - {sidebarOpen && ( -
-
setSidebarOpen(false)}>
-
-
-
- setSidebarOpen(false)}> -
- - - -
-
-

TCG Vault

-

- Hi, {user?.firstName || user?.username} -

-
- - -
-
- - - -
-
-
- {user?.firstName?.[0] || user?.username?.[0]?.toUpperCase() || 'U'} -
-
-

- {user?.firstName && user?.lastName - ? `${user.firstName} ${user.lastName}` - : user?.username} -

-

{user?.email}

-
-
-
-
-
- )} - - {/* Bottom Navigation */} - - - {/* Page Content */} -
-
- {children} -
-
- - {/* 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 - - )} -
- - -
-
- )} -
- - {/* Click outside to close menus */} - {(showUserMenu || sidebarOpen) && ( -
{ - setShowUserMenu(false); - setSidebarOpen(false); - }} - /> - )} -
- ); -}; - -export default ResponsiveLayout; \ No newline at end of file diff --git a/src/components/admin/AdminPanel.tsx b/src/components/admin/AdminPanel.tsx deleted file mode 100644 index fc6ceb7..0000000 --- a/src/components/admin/AdminPanel.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; -import { Navigate } from 'react-router-dom'; -import UserManagement from './UserManagement'; -import CardManagement from './CardManagement'; -import SystemStats from './SystemStats'; -import CardLoader from './CardLoader'; - -type AdminTab = 'dashboard' | 'users' | 'cards' | 'decks' | 'settings'; - -const AdminPanel: React.FC = () => { - const { user, isAdmin } = useAuth(); - const [activeTab, setActiveTab] = useState('dashboard'); - - // Redirect if not admin - if (!isAdmin()) { - return ; - } - - const tabs = [ - { id: 'dashboard' as AdminTab, name: 'Dashboard', icon: '📊' }, - { id: 'users' as AdminTab, name: 'Users', icon: '👥' }, - { id: 'cards' as AdminTab, name: 'Cards', icon: '🃏' }, - { id: 'decks' as AdminTab, name: 'Decks', icon: '📚' }, - { id: 'settings' as AdminTab, name: 'Settings', icon: '⚙️' }, - ]; - - const renderContent = () => { - switch (activeTab) { - case 'dashboard': - return ; - case 'users': - return ; - case 'cards': - return ; - case 'decks': - return
Deck Management - Coming Soon
; - case 'settings': - return
System Settings - Coming Soon
; - default: - return ; - } - }; - - return ( -
- {/* Header */} -
-
-
-
-

Admin Panel

-

- Welcome back, {user?.firstName || user?.username} -

-
-
- - Admin - -
-
-
-
- -
-
- {/* Sidebar Navigation */} -
- - - {/* Quick Stats */} -
-

Quick Stats

-
-
- Total Users - - -
-
- Total Cards - - -
-
- Active Sessions - - -
-
-
-
- - {/* Main Content */} -
-
- {renderContent()} -
-
-
-
-
- ); -}; - -export default AdminPanel; \ No newline at end of file diff --git a/src/components/admin/CardLoader.tsx b/src/components/admin/CardLoader.tsx deleted file mode 100644 index 9f69451..0000000 --- a/src/components/admin/CardLoader.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; - -interface CardCounts { - MTG?: number; - POKEMON?: number; - LORCANA?: number; - total?: number; -} - -interface LoadingResults { - mtg?: number; - pokemon?: number; - lorcana?: number; -} - -const CardLoader: React.FC = () => { - const { user, token } = useAuth(); - const [cardCounts, setCardCounts] = useState({}); - const [loading, setLoading] = useState(false); - const [loadingResults, setLoadingResults] = useState({}); - const [selectedGame, setSelectedGame] = useState('ALL'); - const [message, setMessage] = useState(''); - - // Fetch current card counts - const fetchCardCounts = async () => { - try { - const response = await fetch('https://tcg-vault.vercel.app/api/admin?action=card-counts', { - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (response.ok) { - const data = await response.json(); - setCardCounts(data.counts || {}); - } - } catch (error) { - console.error('Error fetching card counts:', error); - } - }; - - useEffect(() => { - fetchCardCounts(); - }, []); - - // Load cards from external APIs - const loadCards = async (game: string) => { - setLoading(true); - setMessage(`Loading ${game} cards...`); - setLoadingResults({}); - - try { - const response = await fetch(`https://tcg-vault.vercel.app/api/admin?action=load-cards`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ game }) - }); - - if (response.ok) { - const data = await response.json(); - setLoadingResults(data.results || {}); - setMessage(`Successfully loaded cards! ${data.message}`); - - // Refresh card counts - setTimeout(() => { - fetchCardCounts(); - }, 1000); - } else { - const errorData = await response.json(); - setMessage(`Error loading cards: ${errorData.error}`); - } - } catch (error) { - console.error('Error loading cards:', error); - setMessage('Error loading cards. Please try again.'); - } finally { - setLoading(false); - } - }; - - const handleLoadCards = () => { - loadCards(selectedGame); - }; - - return ( -
-

Card Database Loader

- - {/* Current Card Counts */} -
-

Current Database Status

-
-
-
{cardCounts.MTG || 0}
-
MTG Cards
-
-
-
{cardCounts.POKEMON || 0}
-
Pokémon Cards
-
-
-
{cardCounts.LORCANA || 0}
-
Lorcana Cards
-
-
-
{cardCounts.total || 0}
-
Total Cards
-
-
-
- - {/* Load Cards Section */} -
-

Load Cards from External APIs

- -
- - - -
- - {/* Loading Results */} - {Object.keys(loadingResults).length > 0 && ( -
-

Loading Results:

-
- {loadingResults.mtg !== undefined && ( -
MTG: {loadingResults.mtg} cards loaded
- )} - {loadingResults.pokemon !== undefined && ( -
Pokémon: {loadingResults.pokemon} cards loaded
- )} - {loadingResults.lorcana !== undefined && ( -
Lorcana: {loadingResults.lorcana} cards loaded
- )} -
-
- )} - - {/* Message */} - {message && ( -
- {message} -
- )} -
- - {/* Instructions */} -
-

Instructions

-
    -
  • • This will load cards from external APIs into your database
  • -
  • • MTG cards come from Scryfall API
  • -
  • • Pokémon cards come from Pokémon TCG API
  • -
  • • Lorcana cards come from Lorcana API and Lorcast API
  • -
  • • Loading may take several minutes for large datasets
  • -
  • • Cards are deduplicated automatically
  • -
-
-
- ); -}; - -export default CardLoader; \ No newline at end of file diff --git a/src/components/admin/CardManagement.tsx b/src/components/admin/CardManagement.tsx deleted file mode 100644 index fdd1cb8..0000000 --- a/src/components/admin/CardManagement.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -const CardManagement: React.FC = () => { - return ( -
-
-

Card Management

-

Manage card database, pricing, and metadata.

-
- -
-
-
- - - -
-

Card Management Features

-
- -
-

🃏 Card Database: View and manage all cards in the system

-

💰 Pricing Updates: Bulk update card prices from external sources

-

🖼️ Image Management: Upload and manage card images

-

📊 Metadata: Edit card details, sets, and rarity information

-

🔍 Search & Filter: Advanced search and filtering capabilities

-
- -
-

Coming Soon

-

- This feature is currently under development. It will include comprehensive - card management tools for administrators. -

-
-
-
- ); -}; - -export default CardManagement; \ No newline at end of file diff --git a/src/components/admin/SystemStats.tsx b/src/components/admin/SystemStats.tsx deleted file mode 100644 index 728338f..0000000 --- a/src/components/admin/SystemStats.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; - -const SystemStats: React.FC = () => { - return ( -
-
-

System Dashboard

-

Overview of system statistics and health.

-
- - {/* Stats Grid */} -
-
-
-
-

Total Users

-

-

-
-
- - - -
-
-
- -
-
-
-

Total Cards

-

-

-
-
- - - -
-
-
- -
-
-
-

Active Sessions

-

-

-
-
- - - -
-
-
- -
-
-
-

Collections

-

-

-
-
- - - -
-
-
-
- - {/* System Health */} -
-
-

System Health

-
-
- Database Status - - ● Online - -
-
- API Status - - ● Operational - -
-
- Storage - - ● 75% Used - -
-
-
- -
-

Recent Activity

-
-
-
- - - -
-
-

New user registration

-

2 minutes ago

-
-
-
-
- - - -
-
-

Card database updated

-

15 minutes ago

-
-
-
-
- - - -
-
-

System backup completed

-

1 hour ago

-
-
-
-
-
- - {/* Quick Actions */} -
-

Quick Actions

-
- - - - - - - -
-
-
- ); -}; - -export default SystemStats; \ No newline at end of file diff --git a/src/components/admin/UserManagement.tsx b/src/components/admin/UserManagement.tsx deleted file mode 100644 index a0ed0c1..0000000 --- a/src/components/admin/UserManagement.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; - -interface User { - id: number; - username: string; - email: string; - first_name?: string; - last_name?: string; - is_active: boolean; - email_verified: boolean; - roles: string[]; - created_at: string; - last_login?: string; -} - -interface UserResponse { - success: boolean; - users: User[]; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - }; -} - -const UserManagement: React.FC = () => { - const { token } = useAuth(); - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const [searchTerm, setSearchTerm] = useState(''); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [editingUser, setEditingUser] = useState(null); - - const fetchUsers = async (page = 1, search = '') => { - try { - setLoading(true); - const response = await fetch( - `/api/admin/users?page=${page}&limit=20&search=${encodeURIComponent(search)}`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - } - ); - - if (!response.ok) { - throw new Error('Failed to fetch users'); - } - - const data: UserResponse = await response.json(); - setUsers(data.users); - setCurrentPage(data.pagination.page); - setTotalPages(data.pagination.totalPages); - setError(''); - } catch (err) { - setError((err as Error).message); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchUsers(currentPage, searchTerm); - }, [currentPage, token]); - - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - setCurrentPage(1); - fetchUsers(1, searchTerm); - }; - - const handleUserUpdate = async (userId: number, updates: { isActive?: boolean; roles?: string[] }) => { - try { - const response = await fetch(`/api/admin/users?id=${userId}`, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(updates), - }); - - if (!response.ok) { - throw new Error('Failed to update user'); - } - - // Refresh users list - fetchUsers(currentPage, searchTerm); - setEditingUser(null); - } catch (err) { - setError((err as Error).message); - } - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }; - - const getRoleColor = (role: string) => { - switch (role) { - case 'admin': - return 'bg-purple-100 text-purple-800'; - case 'user': - return 'bg-blue-100 text-blue-800'; - default: - return 'bg-gray-100 text-gray-800'; - } - }; - - if (loading && users.length === 0) { - return ( -
-
-
- ); - } - - return ( -
-
-

User Management

-

Manage user accounts, roles, and permissions.

-
- - {/* Search Bar */} -
-
-
- setSearchTerm(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" - /> -
- -
-
- - {error && ( -
- {error} -
- )} - - {/* Users Table */} -
- - - - - - - - - - - - - {users.map((user) => ( - - - - - - - - - ))} - -
- User - - Roles - - Status - - Created - - Last Login - - Actions -
-
-
- {user.first_name || user.last_name - ? `${user.first_name || ''} ${user.last_name || ''}`.trim() - : user.username} -
-
{user.email}
- {(user.first_name || user.last_name) && ( -
@{user.username}
- )} -
-
-
- {user.roles.map((role) => ( - - {role} - - ))} -
-
- - {user.is_active ? 'Active' : 'Inactive'} - - - {formatDate(user.created_at)} - - {user.last_login ? formatDate(user.last_login) : 'Never'} - -
- - -
-
-
- - {/* Pagination */} - {totalPages > 1 && ( -
- -
- )} - - {/* Edit User Modal */} - {editingUser && ( -
-
-

Edit User

-
-
- -
- {['user', 'admin'].map((role) => ( - - ))} -
-
-
-
- - -
-
-
- )} -
- ); -}; - -export default UserManagement; \ No newline at end of file diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx deleted file mode 100644 index 51622a4..0000000 --- a/src/components/auth/LoginForm.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { useAuth } from '../../contexts/AuthContext'; - -const LoginForm: React.FC = () => { - const { login } = useAuth(); - const [formData, setFormData] = useState({ - email: '', - password: '', - }); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [showPassword, setShowPassword] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(null); - - try { - await login(formData.email, formData.password); - } catch (err) { - setError(err instanceof Error ? err.message : 'Login failed'); - } finally { - setIsLoading(false); - } - }; - - const handleChange = (e: React.ChangeEvent) => { - setFormData(prev => ({ - ...prev, - [e.target.name]: e.target.value - })); - }; - - return ( -
- {/* Logo Section */} -
-
- - - -
-

- Welcome Back -

-

- Sign in to your TCG Vault -

-
- - {error && ( -
-
- - - - {error} -
-
- )} - -
-
- -
- - - - -
-
- -
- -
- - -
-
- - - -
-

- Don't have an account?{' '} - - Sign up - -

-
-
-
- ); -}; - -export default LoginForm; \ No newline at end of file diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx deleted file mode 100644 index 1d0bce7..0000000 --- a/src/components/auth/RegisterForm.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { useAuth } from '../../contexts/AuthContext'; - -const RegisterForm: React.FC = () => { - const { register } = useAuth(); - const [formData, setFormData] = useState({ - firstName: '', - lastName: '', - username: '', - email: '', - password: '', - confirmPassword: '', - }); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(null); - - // Validate passwords match - if (formData.password !== formData.confirmPassword) { - setError('Passwords do not match'); - setIsLoading(false); - return; - } - - try { - await register(formData); - } catch (err) { - setError(err instanceof Error ? err.message : 'Registration failed'); - } finally { - setIsLoading(false); - } - }; - - const handleChange = (e: React.ChangeEvent) => { - setFormData(prev => ({ - ...prev, - [e.target.name]: e.target.value - })); - }; - - return ( -
- {/* Logo Section */} -
-
- - - -
-

- Join TCG Vault -

-

- Create your account to get started -

-
- - {error && ( -
-
- - - - {error} -
-
- )} - -
-
-
- - -
-
- - -
-
- -
- - -
- -
- -
- - - - -
-
- -
- -
- - -
-
- -
- -
- - -
-
- - - -
-

- Already have an account?{' '} - - Sign in - -

-
-
-
- ); -}; - -export default RegisterForm; \ No newline at end of file diff --git a/src/components/cards/CardDatabaseBrowser.tsx b/src/components/cards/CardDatabaseBrowser.tsx deleted file mode 100644 index 90b1bde..0000000 --- a/src/components/cards/CardDatabaseBrowser.tsx +++ /dev/null @@ -1,607 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; - -import cardDataService from '../../services/cardDataSources'; -import type { Card } from '../../types'; - -interface CardDatabaseBrowserProps { - isOpen: boolean; - onClose: () => void; - onCardSelect?: (card: Card) => void; -} - -const CardDatabaseBrowser: React.FC = ({ - isOpen, - onClose, - onCardSelect -}) => { - const [searchTerm, setSearchTerm] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [selectedGame, setSelectedGame] = useState(''); - const [selectedCard, setSelectedCard] = useState(null); - const [showCardManager, setShowCardManager] = useState(false); - const [isSearching, setIsSearching] = useState(false); - const [viewMode, setViewMode] = useState<'cards' | 'table'>('cards'); - - const queryClient = useQueryClient(); - - // Debounce search input - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearch(searchTerm); - }, 500); - - return () => clearTimeout(timer); - }, [searchTerm]); - - // Search external APIs - const { data: externalCards = [], isLoading: isSearchingExternal } = useQuery({ - queryKey: ['external-cards', debouncedSearch, selectedGame], - queryFn: async () => { - if (!debouncedSearch.trim()) return []; - setIsSearching(true); - try { - const cards = await cardDataService.searchCards(debouncedSearch, selectedGame || undefined); - console.log(`Found ${cards.length} cards for search "${debouncedSearch}" in game "${selectedGame}"`); - return cards; - } catch (error) { - console.error('Error searching external cards:', error); - return []; - } finally { - setIsSearching(false); - } - }, - enabled: !!debouncedSearch.trim() && isOpen, - staleTime: 5 * 60 * 1000, // 5 minutes - retry: 2, - }); - - // Get random cards for discovery - const { data: randomCards = [] } = useQuery({ - queryKey: ['random-cards', selectedGame], - queryFn: async () => { - try { - const cards = await cardDataService.getRandomCards(20, selectedGame || undefined); - console.log(`Found ${cards.length} random cards for game "${selectedGame}"`); - return cards; - } catch (error) { - console.error('Error fetching random cards:', error); - return []; - } - }, - enabled: isOpen && !debouncedSearch.trim(), - staleTime: 10 * 60 * 1000, // 10 minutes - retry: 2, - }); - - // Add card to database mutation - const addToDatabaseMutation = useMutation({ - mutationFn: async (card: Card) => { - // First, try to add the card to our database - const response = await fetch('/api/cards/find-or-create', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('tcg-vault-token')}`, - }, - body: JSON.stringify({ - name: card.name, - game: card.game, - setName: card.set_name, - setCode: card.set_code, - rarity: card.rarity, - cardType: card.card_type, - manaCost: card.mana_cost, - imageUrl: card.stock_image_url, - }), - }); - - if (!response.ok) { - throw new Error('Failed to add card to database'); - } - - const result = await response.json(); - return result.card; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['cards'] }); - }, - }); - - const handleCardClick = async (card: Card) => { - if (onCardSelect) { - onCardSelect(card); - onClose(); - } else { - setSelectedCard(card); - setShowCardManager(true); - } - }; - - const handleAddToDatabase = async (card: Card) => { - try { - await addToDatabaseMutation.mutateAsync(card); - // Show success message - alert(`${card.name} has been added to the database!`); - } catch (error) { - console.error('Error adding card to database:', error); - alert('Failed to add card to database. Please try again.'); - } - }; - - const getGameBadgeColor = (game: string) => { - switch (game) { - case 'MTG': return 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300'; - case 'POKEMON': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300'; - case 'LORCANA': return 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'; - case 'YUGIOH': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'; - default: return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300'; - } - }; - - const getRarityBadgeColor = (rarity: string) => { - switch (rarity?.toLowerCase()) { - case 'common': return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300'; - case 'uncommon': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'; - case 'rare': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'; - case 'super rare': return 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'; - case 'legendary': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300'; - case 'mythic': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'; - default: return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300'; - } - }; - - if (!isOpen) return null; - - const displayCards = debouncedSearch.trim() ? externalCards : randomCards; - const isLoading = isSearchingExternal || isSearching; - - // Debug logging - console.log('CardDatabaseBrowser state:', { - isOpen, - searchTerm, - debouncedSearch, - selectedGame, - externalCards: externalCards.length, - randomCards: randomCards.length, - displayCards: displayCards.length, - isLoading - }); - - return ( - <> -
-
e.stopPropagation()} - > -
-
- - {/* Header */} -
-

- Card Database Browser -

- -
- - {/* Search Bar */} -
-
- - - -
- setSearchTerm(e.target.value)} - onKeyDown={(e) => e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - placeholder="Search cards from external databases..." - className="w-full pl-10 pr-4 py-3 bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded-xl text-surface-900 dark:text-white placeholder-surface-500 dark:placeholder-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors" - /> -
- - {/* Game Filter */} -
- - - - - -
- - {/* View Toggle */} -
-
- View: - - -
- - {displayCards.length > 0 && ( - - {displayCards.length} card{displayCards.length !== 1 ? 's' : ''} - - )} -
- - {/* Content */} -
- {isLoading ? ( -
-
- - Searching external databases... - -
- ) : displayCards.length === 0 ? ( -
-
🔍
-

- {debouncedSearch.trim() ? 'No cards found' : 'Discover Cards'} -

-

- {debouncedSearch.trim() - ? 'Try adjusting your search terms or game filter' - : 'Search for cards to see results from external databases' - } -

-
- ) : ( - <> - {/* Card View */} - {viewMode === 'cards' && ( -
- {displayCards.map((card) => ( -
-
- {/* Card Image */} -
- {card.stock_image_url ? ( - {card.name} - ) : ( - - - - )} -
- - {/* Card Info */} -
-

- {card.name} -

-

- {card.set_name} • {card.card_number} -

- - {/* Badges */} -
- - {card.game} - - {card.rarity && ( - - {card.rarity} - - )} - {card.current_price && ( - - ${card.current_price} - - )} -
-
- - {/* Actions */} -
- - -
-
-
- ))} -
- )} - - {/* Table View */} - {viewMode === 'table' && ( -
-
- - - - - - - - - - - - - {displayCards.map((card) => ( - - - - - - - - - ))} - -
- Card - - Set - - Game - - Rarity - - Price - - Actions -
-
-
- {card.stock_image_url ? ( - {card.name} - ) : ( - - - - )} -
-
-
- {card.name} -
-
- #{card.card_number} -
-
-
-
- {card.set_name} - - - {card.game} - - - {card.rarity && ( - - {card.rarity} - - )} - - {card.current_price ? `$${card.current_price}` : '-'} - -
- - -
-
-
-
- )} - - )} -
-
-
-
- - {/* Card Manager Modal */} - {selectedCard && showCardManager && ( -
- {/* This would render the CardManager component */} -
setShowCardManager(false)}> -
-
-

- Add {selectedCard.name} to your collection -

-

- This card will be added to your collection with the details you specify. -

-
- - -
-
-
-
-
- )} - - ); -}; - -export default CardDatabaseBrowser; \ No newline at end of file diff --git a/src/components/cards/CardManager.tsx b/src/components/cards/CardManager.tsx deleted file mode 100644 index ca340ea..0000000 --- a/src/components/cards/CardManager.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { tcgApi } from '../../services/tcgApi'; - -import type { Card, CreateCardData } from '../../types'; - -interface CardManagerProps { - isOpen: boolean; - onClose: () => void; - cardId?: string; // For editing existing card - initialCardData?: Card; // For adding card from database -} - -const CardManager: React.FC = ({ - isOpen, - onClose, - cardId, - initialCardData -}) => { - - const queryClient = useQueryClient(); - - const [formData, setFormData] = useState({ - cardId: initialCardData?.id || '', - status: 'owned', - quantity: 1, - condition: 'near_mint', - notes: '', - tags: [], - collectionIds: [], - deckIds: [], - }); - - const [selectedTags, setSelectedTags] = useState([]); - const [selectedCollections, setSelectedCollections] = useState([]); - - - // Queries - const { data: existingCard } = useQuery({ - queryKey: ['user-card', cardId], - queryFn: () => tcgApi.cards.getUserCard(cardId!), - enabled: !!cardId, - }); - - const { data: cardInfo } = useQuery({ - queryKey: ['card-info', formData.cardId], - queryFn: () => tcgApi.cards.getCard(formData.cardId), - enabled: !!formData.cardId, - }); - - const { data: tags = [] } = useQuery({ - queryKey: ['tags'], - queryFn: () => tcgApi.tags.getTags(), - }); - - const { data: collections = [] } = useQuery({ - queryKey: ['collections'], - queryFn: () => tcgApi.collections.getCollections(), - }); - - // Mutations - const addCardMutation = useMutation({ - mutationFn: tcgApi.cards.addCard, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['user-cards'] }); - queryClient.invalidateQueries({ queryKey: ['collections'] }); - onClose(); - }, - }); - - const updateCardMutation = useMutation({ - mutationFn: ({ id, data }: { id: string; data: Partial }) => - tcgApi.cards.updateCard(id, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['user-cards'] }); - queryClient.invalidateQueries({ queryKey: ['user-card', cardId] }); - onClose(); - }, - }); - - // Initialize form with existing card data - useEffect(() => { - if (existingCard) { - setFormData({ - cardId: existingCard.cardId, - status: existingCard.status, - quantity: existingCard.quantity, - condition: existingCard.condition || 'near_mint', - notes: existingCard.notes || '', - tags: existingCard.tags, - collectionIds: existingCard.collectionIds, - }); - setSelectedTags(existingCard.tags); - setSelectedCollections(existingCard.collectionIds); - } - }, [existingCard]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - const submitData = { - ...formData, - tags: selectedTags, - collectionIds: selectedCollections, - }; - - if (cardId && existingCard) { - updateCardMutation.mutate({ id: cardId, data: submitData }); - } else { - addCardMutation.mutate(submitData); - } - }; - - - - if (!isOpen) return null; - - return ( -
-
-
-
- - {/* Header */} -
-

- {cardId ? 'Edit Card' : 'Add Card'} -

- -
- -
- {/* Card Preview */} - {cardInfo && ( -
-
-
- {cardInfo.image_url ? ( - {cardInfo.name} - ) : ( - - - - )} -
-
-

{cardInfo.name}

-

{cardInfo.set_name}

-
- - {cardInfo.game} - - - {cardInfo.rarity} - -
-
-
-
- )} - -
e.stopPropagation()}> - {/* Status Toggle */} -
- -
- - -
-
- - {/* Quantity */} -
- -
- - - {formData.quantity} - - -
-
- - {/* Condition */} - {formData.status === 'owned' && ( -
- - -
- )} - - {/* Tags */} -
- -
- {tags.map((tag) => ( - - ))} -
-
- - {/* Collections */} -
- -
- {collections.map((collection) => ( - - ))} -
-
- - {/* Notes */} -
- -
-
-
-

AI OCR Settings

- -
- -
- {/* Service Selection */} -
- -
- - -
-
- - {/* OpenAI Settings */} -
- - {/* Ollama Settings */} -
-

Ollama Configuration

-
-
- - setOllamaUrl(e.target.value)} - placeholder="http://localhost:11434" - className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" - /> -

- Requires LLaVA or similar vision model: ollama pull llava -

-
-
- - {testResults.ollama && ( - {testResults.ollama} - )} -
-
-
-
- -
- - -
-
-