Back to Blog

Build a Next.js Pokemon Collection Tracker with AI Insights

PokemonPriceTracker Team

6 min read
Build a Next.js Pokemon Collection Tracker with AI Insights

Build a Next.js Pokemon Collection Tracker

What You'll Build

A complete collection management platform with:

  • ✅ User authentication (Clerk)
  • ✅ Card search and add to collection
  • ✅ Portfolio value tracking
  • ✅ AI-powered investment insights (OpenAI)
  • ✅ Price history charts
  • ✅ Collection analytics dashboard

Tech Stack

  • Frontend: Next.js 15 + React
  • Database: MongoDB + Mongoose
  • Auth: Clerk
  • AI: OpenAI GPT-4
  • API: PokemonPriceTracker

Quick Start

npx create-next-app@latest pokemon-tracker
cd pokemon-tracker
npm install mongoose @clerk/nextjs openai axios recharts

Step 1: Database Schema

// src/models/CollectionCard.ts
import mongoose from 'mongoose';

const CollectionCardSchema = new mongoose.Schema({
  userId: {
    type: String,
    required: true,
    index: true
  },
  tcgPlayerId: {
    type: String,
    required: true
  },
  cardName: String,
  setName: String,
  purchasePrice: Number,
  purchaseDate: Date,
  quantity: {
    type: Number,
    default: 1
  },
  condition: {
    type: String,
    enum: ['Mint', 'Near Mint', 'Lightly Played', 'Played'],
    default: 'Near Mint'
  },
  currentPrice: Number,
  lastUpdated: {
    type: Date,
    default: Date.now
  }
});

export const CollectionCard = mongoose.models.CollectionCard ||
  mongoose.model('CollectionCard', CollectionCardSchema);

Step 2: Server Actions

// src/app/actions/collection.ts
'use server';

import { auth } from '@clerk/nextjs';
import { CollectionCard } from '@/models/CollectionCard';
import { connectToDatabase } from '@/lib/mongodb';
import axios from 'axios';
import { revalidatePath } from 'next/cache';

export async function addCardToCollection(data: {
  tcgPlayerId: string;
  purchasePrice?: number;
  quantity?: number;
  condition?: string;
}) {
  const { userId } = auth();

  if (!userId) {
    throw new Error('Unauthorized');
  }

  await connectToDatabase();

  // Fetch current card data from API
  const response = await axios.get(
    `https://www.pokemonpricetracker.com/api/v2/cards/${data.tcgPlayerId}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.POKEMON_API_KEY}`
      }
    }
  );

  const cardData = response.data;

  const collectionCard = await CollectionCard.create({
    userId,
    tcgPlayerId: data.tcgPlayerId,
    cardName: cardData.name,
    setName: cardData.setName,
    purchasePrice: data.purchasePrice || cardData.prices.market,
    purchaseDate: new Date(),
    quantity: data.quantity || 1,
    condition: data.condition || 'Near Mint',
    currentPrice: cardData.prices.market
  });

  revalidatePath('/dashboard/collection');
  return collectionCard;
}

export async function getCollection() {
  const { userId } = auth();

  if (!userId) {
    throw new Error('Unauthorized');
  }

  await connectToDatabase();

  const cards = await CollectionCard.find({ userId }).lean();
  return JSON.parse(JSON.stringify(cards));
}

export async function updatePrices() {
  const { userId } = auth();

  if (!userId) {
    throw new Error('Unauthorized');
  }

  await connectToDatabase();

  const cards = await CollectionCard.find({ userId });

  for (const card of cards) {
    try {
      const response = await axios.get(
        `https://www.pokemonpricetracker.com/api/v2/cards/${card.tcgPlayerId}`,
        {
          headers: {
            'Authorization': `Bearer ${process.env.POKEMON_API_KEY}`
          }
        }
      );

      card.currentPrice = response.data.prices.market;
      card.lastUpdated = new Date();
      await card.save();
    } catch (error) {
      console.error(`Error updating ${card.cardName}:`, error);
    }
  }

  revalidatePath('/dashboard/collection');
}

Step 3: Collection Dashboard

// src/app/dashboard/collection/page.tsx
import { getCollection } from '@/app/actions/collection';
import { AddCardForm } from '@/components/AddCardForm';
import { CollectionGrid } from '@/components/CollectionGrid';
import { PortfolioSummary } from '@/components/PortfolioSummary';

export default async function CollectionPage() {
  const cards = await getCollection();

  const totalInvested = cards.reduce((sum, card) =>
    sum + (card.purchasePrice * card.quantity), 0
  );

  const currentValue = cards.reduce((sum, card) =>
    sum + (card.currentPrice * card.quantity), 0
  );

  const totalGain = currentValue - totalInvested;
  const totalGainPct = (totalGain / totalInvested) * 100;

  return (
    <div className="container mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">My Collection</h1>

      <PortfolioSummary
        totalInvested={totalInvested}
        currentValue={currentValue}
        totalGain={totalGain}
        totalGainPct={totalGainPct}
        cardCount={cards.length}
      />

      <AddCardForm />

      <CollectionGrid cards={cards} />
    </div>
  );
}

Step 4: Add Card Form

// src/components/AddCardForm.tsx
'use client';

import { useState } from 'react';
import { addCardToCollection } from '@/app/actions/collection';
import axios from 'axios';

export function AddCardForm() {
  const [search, setSearch] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  const handleSearch = async () => {
    setLoading(true);
    const response = await axios.post('/api/search-cards', {
      query: search
    });
    setResults(response.data.matches || []);
    setLoading(false);
  };

  const handleAdd = async (card: any) => {
    await addCardToCollection({
      tcgPlayerId: card.tcgPlayerId
    });
    alert('Card added to collection!');
  };

  return (
    <div className="bg-white p-6 rounded-lg shadow mb-6">
      <h2 className="text-xl font-bold mb-4">Add Card</h2>

      <div className="flex gap-2 mb-4">
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Search for a card..."
          className="flex-1 border rounded px-4 py-2"
        />
        <button
          onClick={handleSearch}
          className="bg-blue-600 text-white px-6 py-2 rounded"
        >
          Search
        </button>
      </div>

      {results.length > 0 && (
        <div className="grid grid-cols-3 gap-4">
          {results.map((result: any) => (
            <div key={result.card.tcgPlayerId} className="border rounded p-4">
              <img
                src={result.card.image?.url}
                alt={result.card.name}
                className="w-full mb-2"
              />
              <h3 className="font-bold">{result.card.name}</h3>
              <p className="text-sm text-gray-600">{result.card.setName}</p>
              <p className="text-lg font-bold mt-2">
                ${result.card.prices.market.toFixed(2)}
              </p>
              <button
                onClick={() => handleAdd(result.card)}
                className="w-full bg-green-600 text-white py-2 rounded mt-2"
              >
                Add to Collection
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Step 5: AI Insights with OpenAI

// src/app/actions/ai-insights.ts
'use server';

import OpenAI from 'openai';
import { getCollection } from './collection';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

export async function getAIInsights() {
  const cards = await getCollection();

  // Calculate portfolio stats
  const totalValue = cards.reduce((sum, card) =>
    sum + (card.currentPrice * card.quantity), 0
  );

  const totalInvested = cards.reduce((sum, card) =>
    sum + (card.purchasePrice * card.quantity), 0
  );

  const cardsList = cards.map(card =>
    `${card.cardName} (${card.setName}): Purchased at $${card.purchasePrice}, Current $${card.currentPrice}`
  ).join('\n');

  const prompt = `Analyze this Pokemon card collection and provide investment insights:

Portfolio Value: $${totalValue.toFixed(2)}
Total Invested: $${totalInvested.toFixed(2)}
Total Gain: $${(totalValue - totalInvested).toFixed(2)}

Cards:
${cardsList}

Provide:
1. Overall portfolio health assessment
2. Best performing cards and why
3. Underperforming cards and recommendations
4. Diversification analysis
5. Investment opportunities or selling recommendations

Be specific and actionable.`;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      {
        role: 'system',
        content: 'You are a Pokemon card investment analyst with expertise in market trends and card valuations.'
      },
      {
        role: 'user',
        content: prompt
      }
    ]
  });

  return completion.choices[0].message.content;
}

Step 6: Insights Component

// src/components/AIInsights.tsx
'use client';

import { useState } from 'react';
import { getAIInsights } from '@/app/actions/ai-insights';

export function AIInsights() {
  const [insights, setInsights] = useState('');
  const [loading, setLoading] = useState(false);

  const handleGenerate = async () => {
    setLoading(true);
    const result = await getAIInsights();
    setInsights(result);
    setLoading(false);
  };

  return (
    <div className="bg-gradient-to-r from-purple-50 to-blue-50 p-6 rounded-lg">
      <h2 className="text-2xl font-bold mb-4">🤖 AI Investment Insights</h2>

      {!insights && (
        <button
          onClick={handleGenerate}
          disabled={loading}
          className="bg-purple-600 text-white px-6 py-3 rounded-lg font-semibold"
        >
          {loading ? 'Analyzing...' : 'Generate AI Analysis'}
        </button>
      )}

      {insights && (
        <div className="bg-white p-6 rounded-lg mt-4 prose">
          {insights.split('\n').map((line, i) => (
            <p key={i}>{line}</p>
          ))}
        </div>
      )}
    </div>
  );
}

Deployment

# Build
npm run build

# Deploy to Vercel
vercel

# Set environment variables:
# - MONGODB_URI
# - POKEMON_API_KEY
# - OPENAI_API_KEY
# - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
# - CLERK_SECRET_KEY

Features to Add

  1. Price Alerts: Notify when cards hit target prices
  2. Bulk Import: CSV upload for existing collections
  3. Set Completion: Track which sets you've completed
  4. Trade Value: Calculate fair trade values
  5. Collection Sharing: Public collection pages

Related Tutorials

Get Free API Key →

PokemonPriceTracker Team

Related Articles

Build a PSA Grading ROI Calculator with React - Complete Guide
Tools & Calculators
January 22, 2025

Build a PSA Grading ROI Calculator with React - Complete Guide

Create an interactive PSA grading calculator that analyzes ROI, calculates break-even points, and visualizes grading profitability with real market data.

PokemonPriceTracker Team

Build a ChatGPT Custom GPT for Pokemon Card Price Analysis (2025 Guide)
AI & Development
January 15, 2025

Build a ChatGPT Custom GPT for Pokemon Card Price Analysis (2025 Guide)

Learn how to create a custom ChatGPT GPT that analyzes Pokemon card prices, tracks investments, and provides market insights using natural language. Complete guide with OpenAPI schema and examples.

PokemonPriceTracker Team

PokemonPriceTracker Team

Stay Updated

Subscribe to our newsletter for the latest Pokemon card market trends, investment opportunities, and exclusive insights delivered straight to your inbox.