From f13d852ca1ca58e58ebd19ebc9bede90cf492ed7 Mon Sep 17 00:00:00 2001 From: DanFashauer Date: Wed, 17 Jun 2026 21:41:01 -0400 Subject: [PATCH] Scaffold VaultLens MVP app structure and docs --- .env.example | 3 ++ README.md | 46 +++++++++++++++++++- docs/DATA_PROVIDERS.md | 18 ++++++++ docs/MVP_SCOPE.md | 16 +++++++ docs/PRODUCT_BRIEF.md | 12 ++++++ docs/ROADMAP.md | 27 ++++++++++++ next-env.d.ts | 4 ++ next.config.mjs | 8 ++++ package.json | 27 ++++++++++++ postcss.config.mjs | 6 +++ src/app/api/cards/route.ts | 6 +++ src/app/api/market/route.ts | 9 ++++ src/app/api/scan/route.ts | 7 ++++ src/app/card/[id]/page.tsx | 44 ++++++++++++++++++++ src/app/collection/page.tsx | 28 +++++++++++++ src/app/globals.css | 11 +++++ src/app/layout.tsx | 17 ++++++++ src/app/page.tsx | 24 +++++++++++ src/app/scan/page.tsx | 27 ++++++++++++ src/components/nav.tsx | 19 +++++++++ src/lib/card-identification.ts | 3 ++ src/lib/confidence.ts | 5 +++ src/lib/grading-data.ts | 3 ++ src/lib/market-data.ts | 3 ++ src/lib/mock-collection.ts | 31 ++++++++++++++ src/lib/providers/interfaces.ts | 13 ++++++ src/lib/providers/mock-provider.ts | 41 ++++++++++++++++++ src/types/card.ts | 39 +++++++++++++++++ supabase/schema.sql | 67 ++++++++++++++++++++++++++++++ tailwind.config.ts | 11 +++++ tsconfig.json | 23 ++++++++++ 31 files changed, 597 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 docs/DATA_PROVIDERS.md create mode 100644 docs/MVP_SCOPE.md create mode 100644 docs/PRODUCT_BRIEF.md create mode 100644 docs/ROADMAP.md create mode 100644 next-env.d.ts create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 src/app/api/cards/route.ts create mode 100644 src/app/api/market/route.ts create mode 100644 src/app/api/scan/route.ts create mode 100644 src/app/card/[id]/page.tsx create mode 100644 src/app/collection/page.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/scan/page.tsx create mode 100644 src/components/nav.tsx create mode 100644 src/lib/card-identification.ts create mode 100644 src/lib/confidence.ts create mode 100644 src/lib/grading-data.ts create mode 100644 src/lib/market-data.ts create mode 100644 src/lib/mock-collection.ts create mode 100644 src/lib/providers/interfaces.ts create mode 100644 src/lib/providers/mock-provider.ts create mode 100644 src/types/card.ts create mode 100644 supabase/schema.sql create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d76a042 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= diff --git a/README.md b/README.md index 0e1c824..ffd7321 100644 --- a/README.md +++ b/README.md @@ -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 + ``` diff --git a/docs/DATA_PROVIDERS.md b/docs/DATA_PROVIDERS.md new file mode 100644 index 0000000..70c4729 --- /dev/null +++ b/docs/DATA_PROVIDERS.md @@ -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) diff --git a/docs/MVP_SCOPE.md b/docs/MVP_SCOPE.md new file mode 100644 index 0000000..83d473c --- /dev/null +++ b/docs/MVP_SCOPE.md @@ -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. diff --git a/docs/PRODUCT_BRIEF.md b/docs/PRODUCT_BRIEF.md new file mode 100644 index 0000000..ef23236 --- /dev/null +++ b/docs/PRODUCT_BRIEF.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..01d2cf4 --- /dev/null +++ b/docs/ROADMAP.md @@ -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 diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..84ab714 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,4 @@ +/// +/// + +// NOTE: This file should not be edited diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..b44fec7 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + typedRoutes: true + } +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7b8d090 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..ba80730 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/src/app/api/cards/route.ts b/src/app/api/cards/route.ts new file mode 100644 index 0000000..96d72a3 --- /dev/null +++ b/src/app/api/cards/route.ts @@ -0,0 +1,6 @@ +import { NextResponse } from "next/server"; +import { mockCollection } from "@/lib/mock-collection"; + +export async function GET() { + return NextResponse.json(mockCollection); +} diff --git a/src/app/api/market/route.ts b/src/app/api/market/route.ts new file mode 100644 index 0000000..97a2f83 --- /dev/null +++ b/src/app/api/market/route.ts @@ -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); +} diff --git a/src/app/api/scan/route.ts b/src/app/api/scan/route.ts new file mode 100644 index 0000000..7721b22 --- /dev/null +++ b/src/app/api/scan/route.ts @@ -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); +} diff --git a/src/app/card/[id]/page.tsx b/src/app/card/[id]/page.tsx new file mode 100644 index 0000000..a958ed5 --- /dev/null +++ b/src/app/card/[id]/page.tsx @@ -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 ( +
+
+ ); +} diff --git a/src/app/collection/page.tsx b/src/app/collection/page.tsx new file mode 100644 index 0000000..62c1237 --- /dev/null +++ b/src/app/collection/page.tsx @@ -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 ( +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..f418125 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,11 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: light; +} + +body { + @apply bg-slate-50 text-slate-900; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..e0e8e56 --- /dev/null +++ b/src/app/layout.tsx @@ -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 ( + + +
{children}
+ + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..4dd9901 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,24 @@ +import Link from "next/link"; +import { Nav } from "@/components/nav"; + +export default function HomePage() { + return ( +
+
+ ); +} diff --git a/src/app/scan/page.tsx b/src/app/scan/page.tsx new file mode 100644 index 0000000..9a05c21 --- /dev/null +++ b/src/app/scan/page.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; +import { Nav } from "@/components/nav"; + +export default function ScanPage() { + return ( +
+
+ ); +} diff --git a/src/components/nav.tsx b/src/components/nav.tsx new file mode 100644 index 0000000..61de1b7 --- /dev/null +++ b/src/components/nav.tsx @@ -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 ( + + ); +} diff --git a/src/lib/card-identification.ts b/src/lib/card-identification.ts new file mode 100644 index 0000000..8d6d708 --- /dev/null +++ b/src/lib/card-identification.ts @@ -0,0 +1,3 @@ +import { MockCardIdentificationProvider } from "./providers/mock-provider"; + +export const cardIdentificationProvider = new MockCardIdentificationProvider(); diff --git a/src/lib/confidence.ts b/src/lib/confidence.ts new file mode 100644 index 0000000..1e8875a --- /dev/null +++ b/src/lib/confidence.ts @@ -0,0 +1,5 @@ +export function confidenceBucket(score: number): "low" | "medium" | "high" { + if (score >= 80) return "high"; + if (score >= 60) return "medium"; + return "low"; +} diff --git a/src/lib/grading-data.ts b/src/lib/grading-data.ts new file mode 100644 index 0000000..09ace73 --- /dev/null +++ b/src/lib/grading-data.ts @@ -0,0 +1,3 @@ +import { MockGradingDataProvider } from "./providers/mock-provider"; + +export const gradingDataProvider = new MockGradingDataProvider(); diff --git a/src/lib/market-data.ts b/src/lib/market-data.ts new file mode 100644 index 0000000..b02f5be --- /dev/null +++ b/src/lib/market-data.ts @@ -0,0 +1,3 @@ +import { MockMarketDataProvider } from "./providers/mock-provider"; + +export const marketDataProvider = new MockMarketDataProvider(); diff --git a/src/lib/mock-collection.ts b/src/lib/mock-collection.ts new file mode 100644 index 0000000..307dd7e --- /dev/null +++ b/src/lib/mock-collection.ts @@ -0,0 +1,31 @@ +import type { CollectionCard } from "@/types/card"; + +export const mockCollection: CollectionCard[] = [ + { + card: { + id: "card-1986-jordan-fleer-57", + name: "1986 Fleer Michael Jordan Rookie", + player: "Michael Jordan", + year: 1986, + setName: "Fleer Basketball", + cardNumber: "57", + sport: "Basketball", + confidenceScore: 82 + }, + market: { + rawValueLow: 2200, + rawValueHigh: 3400, + comparableSalesSource: "Mock comparable sales feed", + confidenceLevel: "medium" + }, + grading: [ + { company: "PSA", valueLow: 6500, valueHigh: 12000 }, + { company: "CGC", valueLow: 5200, valueHigh: 9800 } + ], + recommendation: "grade", + ownedQuantity: 1, + conditionEstimate: "Near Mint", + gradedStatus: "ungraded", + notes: "Centering looks strong." + } +]; diff --git a/src/lib/providers/interfaces.ts b/src/lib/providers/interfaces.ts new file mode 100644 index 0000000..c55829e --- /dev/null +++ b/src/lib/providers/interfaces.ts @@ -0,0 +1,13 @@ +import type { CardIdentificationResult, GradingEstimate, MarketEstimate } from "@/types/card"; + +export interface CardIdentificationProvider { + identify(frontImageUrl: string, backImageUrl?: string): Promise; +} + +export interface MarketDataProvider { + estimate(card: CardIdentificationResult): Promise; +} + +export interface GradingDataProvider { + estimate(card: CardIdentificationResult): Promise; +} diff --git a/src/lib/providers/mock-provider.ts b/src/lib/providers/mock-provider.ts new file mode 100644 index 0000000..0021049 --- /dev/null +++ b/src/lib/providers/mock-provider.ts @@ -0,0 +1,41 @@ +import type { CardIdentificationResult, GradingEstimate, MarketEstimate } from "@/types/card"; +import type { CardIdentificationProvider, GradingDataProvider, MarketDataProvider } from "./interfaces"; + +const mockCard: CardIdentificationResult = { + id: "card-1986-jordan-fleer-57", + name: "1986 Fleer Michael Jordan Rookie", + player: "Michael Jordan", + year: 1986, + setName: "Fleer Basketball", + cardNumber: "57", + sport: "Basketball", + confidenceScore: 82 +}; + +export class MockCardIdentificationProvider implements CardIdentificationProvider { + async identify(): Promise { + return Promise.resolve(mockCard); + } +} + +export class MockMarketDataProvider implements MarketDataProvider { + async estimate(): Promise { + return Promise.resolve({ + rawValueLow: 2200, + rawValueHigh: 3400, + comparableSalesSource: "Mock comparable sales feed", + confidenceLevel: "medium" + }); + } +} + +export class MockGradingDataProvider implements GradingDataProvider { + async estimate(): Promise { + return Promise.resolve([ + { company: "PSA", valueLow: 6500, valueHigh: 12000 }, + { company: "CGC", valueLow: 5200, valueHigh: 9800 }, + { company: "SGC", valueLow: 4700, valueHigh: 8700 }, + { company: "BGS", valueLow: 6100, valueHigh: 11100 } + ]); + } +} diff --git a/src/types/card.ts b/src/types/card.ts new file mode 100644 index 0000000..52d187c --- /dev/null +++ b/src/types/card.ts @@ -0,0 +1,39 @@ +export type Recommendation = "hold" | "sell" | "grade" | "research"; + +export interface CardIdentificationResult { + id: string; + name: string; + player: string; + year: number; + setName: string; + cardNumber: string; + sport: string; + confidenceScore: number; +} + +export interface MarketEstimate { + rawValueLow: number; + rawValueHigh: number; + comparableSalesSource: string; + confidenceLevel: "low" | "medium" | "high"; +} + +export interface GradingEstimate { + company: "PSA" | "CGC" | "SGC" | "BGS"; + valueLow: number; + valueHigh: number; +} + +export interface CardInsight { + card: CardIdentificationResult; + market: MarketEstimate; + grading: GradingEstimate[]; + recommendation: Recommendation; +} + +export interface CollectionCard extends CardInsight { + ownedQuantity: number; + conditionEstimate: string; + gradedStatus: "graded" | "ungraded"; + notes?: string; +} diff --git a/supabase/schema.sql b/supabase/schema.sql new file mode 100644 index 0000000..ec531de --- /dev/null +++ b/supabase/schema.sql @@ -0,0 +1,67 @@ +create table if not exists users ( + id uuid primary key, + email text unique not null, + created_at timestamptz default now() +); + +create table if not exists cards ( + id uuid primary key default gen_random_uuid(), + user_id uuid references users(id), + name text not null, + player text, + year int, + set_name text, + card_number text, + sport text, + manufacturer text, + variant text, + image_url text, + condition_estimate text, + raw_value_low numeric, + raw_value_high numeric, + graded_value_low numeric, + graded_value_high numeric, + confidence_score numeric, + recommendation text, + created_at timestamptz default now(), + updated_at timestamptz default now() +); + +create table if not exists collections ( + id uuid primary key default gen_random_uuid(), + user_id uuid references users(id), + name text default 'Default Collection', + created_at timestamptz default now() +); + +create table if not exists card_scans ( + id uuid primary key default gen_random_uuid(), + user_id uuid references users(id), + card_id uuid references cards(id), + front_image_url text, + back_image_url text, + scan_status text, + created_at timestamptz default now() +); + +create table if not exists market_estimates ( + id uuid primary key default gen_random_uuid(), + card_id uuid references cards(id), + provider text, + value_low numeric, + value_high numeric, + confidence_level text, + metadata jsonb, + created_at timestamptz default now() +); + +create table if not exists grading_estimates ( + id uuid primary key default gen_random_uuid(), + card_id uuid references cards(id), + grading_company text, + grade text, + value_low numeric, + value_high numeric, + metadata jsonb, + created_at timestamptz default now() +); diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..8337894 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {} + }, + plugins: [] +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..caa4325 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}