-
Notifications
You must be signed in to change notification settings - Fork 0
Scaffold VaultLens MVP foundation with mock providers and docs #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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= |
| 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 | ||
| ``` |
| 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) |
| 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. |
| 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. |
| 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 |
| 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 |
| 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; |
| 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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export default { | ||
| plugins: { | ||
| tailwindcss: {}, | ||
| autoprefixer: {} | ||
| } | ||
| }; |
| 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); | ||
| } |
| 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); | ||
| } |
| 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); | ||
| } |
| 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]; | ||
|
|
||
| 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> | ||
| ); | ||
| } | ||
| 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> | ||
| ); | ||
| } |
| 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; | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With Useful? React with 👍 / 👎. |
||
| {link.label} | ||
| </Link> | ||
| ))} | ||
| </nav> | ||
| ); | ||
| } | ||
| 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(); |
| 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"; | ||
| } |
| 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(); |
| 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the requested
params.idis not in the collection, this falls back tomockCollection[0], so/card/anything-elserenders 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; usenotFound()or an explicit missing-card state instead of showing another card's details.Useful? React with 👍 / 👎.