Skip to content

x0cfwm/slabhub

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

318 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SlabHub - One Piece TCG Seller/Collector CRM

A production-quality monorepo for managing One Piece TCG inventory, pricing, and public vendor pages.

πŸ“ Project Structure

/
β”œβ”€β”€ apps/
β”‚   β”œβ”€β”€ api/               # NestJS backend API
β”‚   β”‚   └── src/
β”‚   β”‚       β”œβ”€β”€ modules/
β”‚   β”‚       β”‚   β”œβ”€β”€ auth/      # Authentication (minimal Stage 1)
β”‚   β”‚       β”‚   β”œβ”€β”€ cards/     # Card catalog endpoints
β”‚   β”‚       β”‚   β”œβ”€β”€ health/    # Health check endpoint
β”‚   β”‚       β”‚   β”œβ”€β”€ inventory/ # Inventory CRUD
β”‚   β”‚       β”‚   β”œβ”€β”€ pricing/   # Pricing endpoints
β”‚   β”‚       β”‚   β”œβ”€β”€ prisma/    # Database service
β”‚   β”‚       β”‚   β”œβ”€β”€ profile/   # Seller profile endpoints
β”‚   β”‚       β”‚   └── vendor/    # Public vendor page
β”‚   β”‚       β”œβ”€β”€ app.module.ts
β”‚   β”‚       └── main.ts
β”‚   β”‚
β”‚   └── web/               # Next.js frontend
β”‚       └── src/
β”‚           β”œβ”€β”€ app/
β”‚           β”œβ”€β”€ components/
β”‚           └── lib/
β”‚
β”œβ”€β”€ packages/
β”‚   └── shared/           # Shared types (future use)
β”‚
β”œβ”€β”€ infra/
β”‚   └── docker/           # Docker compose for PostgreSQL
β”‚
β”œβ”€β”€ prisma/
β”‚   β”œβ”€β”€ schema.prisma     # Database schema
β”‚   β”œβ”€β”€ migrations/       # Migration history
β”‚   └── seed.ts           # Seed script
β”‚
└── package.json          # Root monorepo scripts

πŸš€ Quick Start

Prerequisites

  • Node.js 18+
  • pnpm 9+
  • Docker (for PostgreSQL)

1. Install Dependencies

pnpm install

2. Start PostgreSQL

pnpm db:up

This starts PostgreSQL on localhost:5432 with:

  • User: postgres
  • Password: postgres_password
  • Database: slabhub

3. Generate Prisma Client

pnpm prisma:generate

4. Run Migrations

pnpm prisma:migrate

5. Seed the Database

pnpm seed

This creates:

  • 1 Demo Seller Profile (nami-treasures)
  • 30 One Piece TCG Card Profiles
  • 90 Card Variants (3 per card)
  • 30 Pricing Snapshots
  • 18 Inventory Items (10 RAW, 5 GRADED, 3 SEALED)

6. Start Development Servers

# Start both API and Web
pnpm dev

# Or start individually
pnpm dev:api   # API on http://localhost:3001
pnpm dev:web   # Web on http://localhost:3000

πŸ“‹ Available Scripts

Script Description
pnpm dev Run API + Web in parallel
pnpm dev:api Run API only
pnpm dev:web Run Web only
pnpm build Build all packages
pnpm db:up Start PostgreSQL container
pnpm db:down Stop PostgreSQL container
pnpm db:logs View PostgreSQL logs
pnpm prisma:generate Generate Prisma Client
pnpm prisma:migrate Run migrations (dev)
pnpm prisma:studio Open Prisma Studio
pnpm seed Seed the database
pnpm pricecharting:crawl:onepiece Run PriceCharting One Piece crawler
pnpm pricecharting:cleanup:links Clean up referral links in PriceCharting sales data

πŸ“ˆ PriceCharting Crawler

The PriceCharting crawler is a robust ingestion pipeline for One Piece TCG cards.

Commands

Command Description
pnpm pricecharting:crawl:onepiece Start a full crawl of One Piece cards
pnpm pricecharting:cleanup:links Clean up existing referral/tracking links in the database
... --dryRun Fetch/Parse/Cleanup but do not save to database
... --maxProducts 10 Limit the number of products for testing
... --linkRefProducts Link crawled URLs to existing RefProduct records
... --onlySetSlug <slug> Only crawl a specific set (e.g. one-piece-500-years-in-the-future)

Configuration & Features

  • Rate Limiting: Implementation ensures max 1 request per second to respect the target site.
  • Resilience: Automatic retries (3 times) with exponential backoff on 429/5xx errors.
  • Idempotency: Products are upserted based on their canonical URL; re-running the crawler will update existing records.
  • Data Capture: Stores full "Details" block as JSON, along with normalized TCGPlayerID, PriceChartingID, and Card Number.
  • Traversal: Automatically discovers set pages from the category entrypoint and handles pagination within sets.

Base URL: http://localhost:3001

Health

Method Endpoint Description
GET /health Health check with DB status

Profile (authenticated)

Method Endpoint Description
GET /v1/me Get current seller profile
PATCH /v1/me Update seller profile

Cards (catalog)

Method Endpoint Description
GET /v1/cards List all cards
GET /v1/cards?query=luffy Search cards
GET /v1/cards/:id Get card by ID
GET /v1/cards/:id/variants Get card variants

Inventory (authenticated)

Method Endpoint Description
GET /v1/inventory List seller's inventory
GET /v1/inventory/:id Get inventory item
POST /v1/inventory Create inventory item
PATCH /v1/inventory/:id Update inventory item
DELETE /v1/inventory/:id Delete inventory item

Pricing

Method Endpoint Description
GET /v1/pricing List all pricing
POST /v1/pricing/refresh Refresh all prices

Vendor (public)

Method Endpoint Description
GET /v1/vendor/:handle Get vendor page data

πŸ” Authentication (Stage 1)

For Stage 1, authentication is header-based:

# Use x-user-handle header
curl -H "x-user-handle: nami-treasures" http://localhost:3001/v1/me

# Or x-user-id header
curl -H "x-user-id: <seller-id>" http://localhost:3001/v1/me

# If no header, defaults to "nami-treasures" demo seller

πŸ“ Example curl Commands

Health Check

curl http://localhost:3001/health

Get Profile

curl http://localhost:3001/v1/me

Update Profile

curl -X PATCH http://localhost:3001/v1/me \
  -H "Content-Type: application/json" \
  -d '{"shopName": "Updated Shop Name", "meetupsEnabled": true}'

List Cards

curl http://localhost:3001/v1/cards

# With search
curl "http://localhost:3001/v1/cards?query=luffy"

Get Card

curl http://localhost:3001/v1/cards/op01-001

List Inventory

curl http://localhost:3001/v1/inventory

Create Inventory Item (RAW)

curl -X POST http://localhost:3001/v1/inventory \
  -H "Content-Type: application/json" \
  -d '{
    "itemType": "SINGLE_CARD_RAW",
    "cardVariantId": "<variant-id>",
    "condition": "NM",
    "quantity": 1,
    "stage": "ACQUIRED",
    "acquisitionPrice": 25
  }'

Create Inventory Item (GRADED)

curl -X POST http://localhost:3001/v1/inventory \
  -H "Content-Type: application/json" \
  -d '{
    "itemType": "SINGLE_CARD_GRADED",
    "cardVariantId": "<variant-id>",
    "gradeProvider": "PSA",
    "gradeValue": "10",
    "certNumber": "CERT-12345",
    "quantity": 1,
    "stage": "LISTED",
    "acquisitionPrice": 150,
    "listingPrice": 299
  }'

Create Inventory Item (SEALED)

curl -X POST http://localhost:3001/v1/inventory \
  -H "Content-Type: application/json" \
  -d '{
    "itemType": "SEALED_PRODUCT",
    "productName": "Romance Dawn Booster Box",
    "productType": "BOOSTER_BOX",
    "language": "JP",
    "integrity": "MINT",
    "quantity": 1,
    "stage": "IN_STOCK",
    "acquisitionPrice": 120,
    "configuration": {
      "containsBoosters": true,
      "packsPerUnit": 24,
      "containsFixedCards": false,
      "containsPromo": false
    }
  }'

Update Inventory Item

curl -X PATCH http://localhost:3001/v1/inventory/<item-id> \
  -H "Content-Type: application/json" \
  -d '{"stage": "LISTED", "listingPrice": 45.99}'

Delete Inventory Item

curl -X DELETE http://localhost:3001/v1/inventory/<item-id>

Get Pricing

curl http://localhost:3001/v1/pricing

Refresh Pricing

curl -X POST http://localhost:3001/v1/pricing/refresh

Get Vendor Page

curl http://localhost:3001/v1/vendor/nami-treasures

πŸ”„ Frontend Integration Mapping

mockApi Function Real API Endpoint
getCurrentUser() GET /v1/me
updateProfile(patch) PATCH /v1/me
listCardProfiles(query) GET /v1/cards?query=...
getCardProfile(id) GET /v1/cards/:id
listInventory() GET /v1/inventory
createInventoryItem(item) POST /v1/inventory
updateInventoryItem(id, patch) PATCH /v1/inventory/:id
deleteInventoryItem(id) DELETE /v1/inventory/:id
listPricing() GET /v1/pricing
refreshPricing() POST /v1/pricing/refresh
(new) Vendor page GET /v1/vendor/:handle

πŸ“Š Database Schema

Entities

  • SellerProfile: User/seller account with shop info
  • CardProfile: Global card catalog (One Piece TCG)
  • CardVariant: Specific variants (language, foil type)
  • PricingSnapshot: Global pricing per card
  • InventoryItem: Seller's inventory (RAW, GRADED, SEALED)

Key Rules

  1. PricingSnapshot is GLOBAL per CardProfile
  2. InventoryItem references CardVariant (not CardProfile directly)
  3. Market price is derived from PricingSnapshot via CardVariant β†’ CardProfile

Inventory Stages

  • ACQUIRED - Just purchased
  • IN_TRANSIT - Shipping to seller
  • BEING_GRADED - At grading company
  • AUTHENTICATED - Verified authentic
  • IN_STOCK - Ready but not listed
  • LISTED - For sale publicly
  • SOLD - Transaction complete
  • ARCHIVED - No longer active

Item Types

  • SINGLE_CARD_RAW - Ungraded single card
  • SINGLE_CARD_GRADED - Professionally graded card
  • SEALED_PRODUCT - Unopened product

πŸ›  Tech Stack

  • Backend: NestJS, TypeScript, Prisma
  • Database: PostgreSQL
  • Frontend: Next.js, React, TailwindCSS, Shadcn/UI
  • Infrastructure: Docker, pnpm workspaces

πŸ” Auth flow (Email OTP)

SlabHub uses a session-based authentication with email OTP (magic codes).

How it works:

  1. Request OTP: User enters email on /login.
  2. Generate & Store: Backend generates a 6-digit code, hashes it, and stores it in the OtpChallenge table.
  3. Send: In development, the OTP is printed to the server console.
  4. Verify: User enters the code on /otp.
  5. Session: Backend verifies the code, upserts the user, and creates a session record. A secure HttpOnly cookie is set in the browser.
  6. Current User: The GET /v1/me endpoint returns the current user based on the session cookie.

Local Testing:

  1. Ensure you have the following in your .env:
    SESSION_COOKIE_NAME=slabhub_session
    OTP_TTL_MINUTES=10
    OTP_SECRET=dev-secret
  2. Start the API and Web app.
  3. Navigate to http://localhost:3000/login.
  4. Enter any email.
  5. Check the API terminal output for the OTP code.
  6. Enter the code on the OTP page.
  7. You should be redirected to the dashboard.

Note: Facebook and Google login buttons are currently stubs for UI demonstration.

πŸ–Ό Media Layer (S3-compatible)

SlabHub stores binary files in DigitalOcean Spaces (or any S3-compatible storage) with content-hash based deduplication.

Setup Environment Variables

Add these to your .env:

S3_ENDPOINT=https://nyc3.digitaloceanspaces.com
S3_REGION=nyc3
S3_BUCKET=slabhub-files-dev
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_PUBLIC_BASE_URL=https://slabhub-files-dev.nyc3.digitaloceanspaces.com # Public origin
S3_CDN_BASE_URL=https://slabhub-files-dev.nyc3.cdn.digitaloceanspaces.com   # CDN origin
S3_FORCE_PATH_STYLE=false

Features:

  • Deduplication: Same photo uploaded twice only takes space once.
  • CDN Support: URLs automatically use the CDN base if configured.
  • Robust Ingestion: PriceCharting images are automatically stored in the media layer.

πŸ“œ License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages