Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,46 @@
# VaultLens
Collector intelligence app for identifying, valuing, and tracking trading cards and collectibles.
VaultLens is a collector intelligence app for identifying, valuing, and tracking trading cards and collectibles.
The goal is to help collectors understand:
- what they own
- what it may be worth
- why it may be worth that amount
- whether it may be worth grading, selling, holding, or researching further
## MVP Focus
The first version focuses on sports cards.
Core features:
- upload/scan card images
- identify card metadata
- estimate raw market value
- compare graded value ranges
- save cards to a personal collection
- provide confidence scoring and next-step recommendations
## Product Principle
VaultLens should not simply guess a price.
It should explain the valuation:
- comparable sales
- condition assumptions
- grading context
- population/scarcity context
- confidence level
## Initial Tech Stack
- Next.js
- TypeScript
- Tailwind CSS
- Supabase
- Vercel
## Status
Early MVP.

## Local Setup
1. Install dependencies:
```bash
npm install
```
2. Copy environment file:
```bash
cp .env.example .env.local
```
3. Start development server:
```bash
npm run dev
```
18 changes: 18 additions & 0 deletions docs/DATA_PROVIDERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# DATA PROVIDERS

VaultLens uses a provider architecture so data sources can be swapped without rewriting product workflows.

## Provider Interfaces
- `CardIdentificationProvider`
- `MarketDataProvider`
- `GradingDataProvider`

## Why
- Marketplace APIs and grading APIs have limits and access constraints.
- Mock providers unblock UX and product iteration.
- Real providers can be implemented behind the same interfaces later.

## Planned Real Integrations
- eBay discovery/detail (where available)
- PSA cert/card context
- Population context providers (CGC/PSA/BGS as available)
16 changes: 16 additions & 0 deletions docs/MVP_SCOPE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# MVP SCOPE

## In Scope
1. User auth-ready structure (Supabase-ready).
2. Scan/upload flow for front/back card images.
3. Mock card identification result.
4. Card detail page with metadata.
5. Market value range (mock).
6. Grading insights (mock).
7. Confidence score and recommendation.
8. Collection tracker with local mock persistence.

## Out of Scope (for now)
- Production OCR/computer vision.
- Live sold-comparable integrations.
- Native mobile apps.
12 changes: 12 additions & 0 deletions docs/PRODUCT_BRIEF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# PRODUCT BRIEF

## Vision
VaultLens is a collector intelligence platform that helps hobbyists understand what they own, what it may be worth, and what action makes sense next.

## MVP Focus
- Sports cards only.
- Web app / PWA style UX first.
- Mock provider architecture for identification, market, and grading data.

## Differentiator
VaultLens does not only show a number. It explains value range, confidence, and recommended next action.
27 changes: 27 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# ROADMAP

## Phase 1 (Current)
- Next.js scaffold
- Landing, scan, card detail, collection pages
- Mock provider interfaces and implementations

## Phase 2
- Confidence model breakdown and explainability
- "Why this value?" module
- Worth grading decision helper

## Phase 3
- Collection tracking expansion (quantity, grade, notes, value totals)
- Supabase persistence and auth

## Planned GitHub Issues
1. Scaffold Next.js app and landing page
2. Build scan/upload flow
3. Add mock card identification provider
4. Add card result page
5. Add market value and grading insight UI
6. Add confidence score logic
7. Add collection tracking
8. Add Supabase schema
9. Add real marketplace provider interface
10. Add grading/population provider interface
4 changes: 4 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
8 changes: 8 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
typedRoutes: true
}
};

export default nextConfig;
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "vaultlens",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.33",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "20.17.30",
"@types/react": "18.3.23",
"@types/react-dom": "18.3.7",
"autoprefixer": "10.4.21",
"eslint": "8.57.1",
"eslint-config-next": "14.2.33",
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
}
}
6 changes: 6 additions & 0 deletions postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
6 changes: 6 additions & 0 deletions src/app/api/cards/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse } from "next/server";
import { mockCollection } from "@/lib/mock-collection";

export async function GET() {
return NextResponse.json(mockCollection);
}
9 changes: 9 additions & 0 deletions src/app/api/market/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
import { marketDataProvider } from "@/lib/market-data";
import { cardIdentificationProvider } from "@/lib/card-identification";

export async function GET() {
const card = await cardIdentificationProvider.identify("mock-front");
const market = await marketDataProvider.estimate(card);
return NextResponse.json(market);
}
7 changes: 7 additions & 0 deletions src/app/api/scan/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from "next/server";
import { cardIdentificationProvider } from "@/lib/card-identification";

export async function POST() {
const card = await cardIdentificationProvider.identify("mock-front");
return NextResponse.json(card);
}
44 changes: 44 additions & 0 deletions src/app/card/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Nav } from "@/components/nav";
import { confidenceBucket } from "@/lib/confidence";
import { mockCollection } from "@/lib/mock-collection";

export default function CardDetailPage({ params }: { params: { id: string } }) {
const result = mockCollection.find((item) => item.card.id === params.id) ?? mockCollection[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return a 404 for unknown card ids

When the requested params.id is not in the collection, this falls back to mockCollection[0], so /card/anything-else renders the Michael Jordan card as if it matched the URL. That can mislead users and later leak or misattribute collection data once these IDs come from persistence; use notFound() or an explicit missing-card state instead of showing another card's details.

Useful? React with 👍 / 👎.


return (
<div>
<Nav />
<section className="rounded-xl border bg-white p-6 shadow-sm">
<h2 className="text-2xl font-semibold">{result.card.name}</h2>
<p className="mt-2 text-sm text-slate-700">
{result.card.year} · {result.card.setName} · #{result.card.cardNumber} · {result.card.player} · {result.card.sport}
</p>

<div className="mt-6 grid gap-4 md:grid-cols-2">
<article className="rounded-lg border p-4">
<h3 className="font-semibold">Market value estimate</h3>
<p className="mt-2 text-sm">${result.market.rawValueLow.toLocaleString()} – ${result.market.rawValueHigh.toLocaleString()}</p>
<p className="text-xs text-slate-600">Source: {result.market.comparableSalesSource}</p>
</article>

<article className="rounded-lg border p-4">
<h3 className="font-semibold">Confidence score</h3>
<p className="mt-2 text-sm">{result.card.confidenceScore}/100 ({confidenceBucket(result.card.confidenceScore)})</p>
<p className="text-xs text-slate-600">Recommendation: {result.recommendation}</p>
</article>
</div>

<article className="mt-4 rounded-lg border p-4">
<h3 className="font-semibold">Grading context</h3>
<ul className="mt-2 grid gap-2 text-sm md:grid-cols-2">
{result.grading.map((grade) => (
<li key={grade.company}>
{grade.company}: ${grade.valueLow.toLocaleString()} – ${grade.valueHigh.toLocaleString()}
</li>
))}
</ul>
</article>
</section>
</div>
);
}
28 changes: 28 additions & 0 deletions src/app/collection/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Link from "next/link";
import { Nav } from "@/components/nav";
import { mockCollection } from "@/lib/mock-collection";

export default function CollectionPage() {
return (
<div>
<Nav />
<section className="rounded-xl border bg-white p-6 shadow-sm">
<h2 className="text-2xl font-semibold">My Collection</h2>
<ul className="mt-4 space-y-3">
{mockCollection.map((item) => (
<li key={item.card.id} className="rounded-md border p-3">
<p className="font-semibold">{item.card.name}</p>
<p className="text-sm text-slate-600">
Qty {item.ownedQuantity} · Estimated ${item.market.rawValueLow.toLocaleString()}–$
{item.market.rawValueHigh.toLocaleString()}
</p>
<Link href={`/card/${item.card.id}`} className="mt-2 inline-block text-sm font-medium underline">
View details
</Link>
</li>
))}
</ul>
</section>
</div>
);
}
11 changes: 11 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
color-scheme: light;
}

body {
@apply bg-slate-50 text-slate-900;
}
17 changes: 17 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "VaultLens",
description: "Collector intelligence for sports cards"
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<main className="mx-auto min-h-screen w-full max-w-5xl p-4 md:p-8">{children}</main>
</body>
</html>
);
}
24 changes: 24 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Link from "next/link";
import { Nav } from "@/components/nav";

export default function HomePage() {
return (
<div>
<Nav />
<section className="rounded-xl border bg-white p-6 shadow-sm">
<h1 className="text-3xl font-bold">VaultLens</h1>
<p className="mt-3 text-slate-700">
Collector intelligence platform for understanding what your cards are worth, why, and what to do next.
</p>
<div className="mt-6 flex flex-wrap gap-3">
<Link href="/scan" className="rounded-md bg-slate-900 px-4 py-2 text-white">
Start a scan
</Link>
<Link href="/collection" className="rounded-md border px-4 py-2">
View collection
</Link>
</div>
</section>
</div>
);
}
27 changes: 27 additions & 0 deletions src/app/scan/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from "next/link";
import { Nav } from "@/components/nav";

export default function ScanPage() {
return (
<div>
<Nav />
<section className="rounded-xl border bg-white p-6 shadow-sm">
<h2 className="text-2xl font-semibold">Scan / Upload Card</h2>
<p className="mt-2 text-sm text-slate-600">Upload front image and optional back image. MVP uses a mock provider.</p>
<form className="mt-6 space-y-4">
<label className="block text-sm font-medium">
Front image
<input type="file" className="mt-1 block w-full rounded-md border p-2" />
</label>
<label className="block text-sm font-medium">
Back image (optional)
<input type="file" className="mt-1 block w-full rounded-md border p-2" />
</label>
<Link href="/card/card-1986-jordan-fleer-57" className="inline-block rounded-md bg-slate-900 px-4 py-2 text-white">
Run mock identification
</Link>
</form>
</section>
</div>
);
}
19 changes: 19 additions & 0 deletions src/components/nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Link from "next/link";

const links = [
{ href: "/", label: "Home" },
{ href: "/scan", label: "Scan" },
{ href: "/collection", label: "Collection" }
];

export function Nav() {
return (
<nav className="mb-6 flex gap-4 rounded-lg border bg-white p-3 text-sm shadow-sm">
{links.map((link) => (
<Link key={link.href} className="font-medium text-slate-700 hover:text-slate-900" href={link.href}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve typed route literals in nav links

With experimental.typedRoutes enabled in next.config.mjs, Next's generated <Link> type rejects arbitrary string hrefs. This mutable links array widens each href to string, so href={link.href} fails type checking during next build even though the literal values are valid routes; keep the array as const/satisfies a route type or cast before passing to <Link>.

Useful? React with 👍 / 👎.

{link.label}
</Link>
))}
</nav>
);
}
3 changes: 3 additions & 0 deletions src/lib/card-identification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { MockCardIdentificationProvider } from "./providers/mock-provider";

export const cardIdentificationProvider = new MockCardIdentificationProvider();
5 changes: 5 additions & 0 deletions src/lib/confidence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function confidenceBucket(score: number): "low" | "medium" | "high" {
if (score >= 80) return "high";
if (score >= 60) return "medium";
return "low";
}
3 changes: 3 additions & 0 deletions src/lib/grading-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { MockGradingDataProvider } from "./providers/mock-provider";

export const gradingDataProvider = new MockGradingDataProvider();
3 changes: 3 additions & 0 deletions src/lib/market-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { MockMarketDataProvider } from "./providers/mock-provider";

export const marketDataProvider = new MockMarketDataProvider();
Loading