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
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
- Price Alerts: Notify when cards hit target prices
- Bulk Import: CSV upload for existing collections
- Set Completion: Track which sets you've completed
- Trade Value: Calculate fair trade values
- Collection Sharing: Public collection pages
Related Tutorials
PokemonPriceTracker Team
Related Articles

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

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
Stay Updated
Subscribe to our newsletter for the latest Pokemon card market trends, investment opportunities, and exclusive insights delivered straight to your inbox.