尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗A premium, dark-first web app for tracking the market value and true investment performance of a Pokémon collection — singles, sealed product and graded cards.
Built with Next.js (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui-style Radix primitives · Supabase · TCG API via the official tcgapi SDK · Recharts · Lucide.
The app runs immediately in demo mode with zero configuration. Demo data is sample data, clearly labeled everywhere, and is never mixed into a real collection.
npm install
npm run dev # http://localhost:3000 — demo mode
npm test # investment-calculation unit tests
npm run typecheck && npm run lint
| Mode | When | Storage |
|---|---|---|
| Demo | Default for signed-out visitors | Sample dataset regenerated relative to today + sandbox edits in localStorage (holo:demo:v1). Product ids use the sample: namespace. |
| My collection — browser | "Use my collection" without Supabase / sign-in | localStorage (holo:local:v1). Real TCG API products. |
| My collection — synced | Supabase configured and signed in | Supabase Postgres with row-level security. |
Switch modes from the sidebar (desktop) or Settings.
Copy .env.example to .env.local.
| Variable | Scope | Purpose |
|---|---|---|
TCGAPI_KEY | server | TCG API key. Enables live search, prices, history and card scanning. Never sent to the browser. |
TCGAPI_GAMES | server | Optional comma-separated game slugs to search. By default every Pokémon catalog (English, Japanese, …) and Disney Lorcana found via GET /games is searchable; the search panel picks Pokémon, Lorcana or both. |
TCGAPI_GAME | server | Game slug for top movers, default pokemon. |
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY | public | Supabase project. Data is protected by RLS. |
SUPABASE_SERVICE_ROLE_KEY | server | Writes to the shared catalog/price tables and runs the daily sync. |
CRON_SECRET | server | Authenticates the Vercel Cron call to /api/cron/sync-prices. |
APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, APNS_PRIVATE_KEY | server | Push notifications to the iPhone app (price targets hit). Optional; see ios/README.md. |
supabase/migrations/ in order (supabase db push, or paste each into the SQL editor). 20261025000000_share_links.sql enables read-only share links (Settings → Share; public pages at /s/<token>).https://<your-domain>/auth/callback, https://*-<your-team>.vercel.app/auth/callback (previews) and http://localhost:3000/auth/callback as redirect URLs. Sign-in uses email magic links that work in any browser (implicit-flow links, finished by /auth/finish); sessions last until sign-out.
{{ .Token }} so the email also carries a code. The login page accepts it, which works even when the link opens in a different browser (e.g. a mail app's in-app browser).src/lib/supabase/database.types.ts with npx supabase gen types typescript if you change the schema.Import the repo, add the env vars, deploy. vercel.json schedules the daily price sync (17 9 * * * UTC); Vercel sends Authorization: Bearer $CRON_SECRET automatically.
All calculations live in src/lib/finance as pure, tested functions.
null, shown as "—", excluded from value and gains, and called out in the UI. It is never treated as $0.Audit trail. Transactions are append-only. A correction is a new row pointing at the original (correctsId) with replacement values; quantity 0 voids. Supabase RLS has no update/delete policies on transactions.
Prices over time. PriceResolver uses only real observations: the current quote for today, stored daily snapshots (carried forward at most 10 days, then treated as unknown), yesterday's price recovered from the source's own 24h change, or a manual valuation for graded cards. It never interpolates or back-fills history.
Unit tests: src/lib/finance/__tests__/finance.test.ts covers the "$1,000 box at market = $0 gain" case, FIFO partial sales, fee loading, corrections/voids, missing prices, TWR with large mid-period purchases, sales, and newly-priced items.
src/lib/pricing/types.ts — PricingProvider interface (search, product, prices per printing, history, top movers) and normalized NormalizedPrice (market, low, 24h/7d/30d %, source update time, fetch time, source, live | stale | unavailable).src/lib/pricing/tcgapi.ts — TCG API implementation using the official SDK (GET /v1/search, /v1/cards/:id, /v1/cards/:id/prices, /v1/cards/:id/history, /v1/prices/top-movers, Pro-only /v1/bulk/prices with automatic fallback). Sealed vs. singles uses type=Sealed Products / Cards.
rate_limit budget from responses (shown in Settings) and backs off briefly after a 429, honoring Retry-After up to 10 minutes.${source}:${externalId} with the exact printing; nothing is matched by name. Another source plugs in by implementing PricingProvider and registering it in src/lib/pricing/index.ts — the collection tables don't change./api/scan)Photos go to TCG API's POST /v1/identify (one prepaid scan each, separate from the daily request limit). The route keeps the key server-side, requires sign-in when Supabase is configured, and rate-limits each client so a burst of rapid scans works but nobody can drain the balance. GET /api/scan returns the scans left (free). Scanning is in Add to Collection ("Scan cards", for adding a stack at once), the Deal page and the Trade checker.
/api/tcg/products/:id/ebay)GET /v1/cards/:id/prices?include=ebay_sales (TCG API Pro plan and up, 2 request credits a call) gives the last 30 days of eBay sales: the median, count and last sale for ungraded copies and each grade, plus the listings. It's fetched only when needed and cached for 12 hours in the server, at the CDN and on the device. EBAY_SOURCE in src/lib/ebay-sample.ts is "tcgapi"; demo products always get sample sales.
Slabs are valued from the same data (GRADED_SOURCE = "ebay" in src/lib/graded.ts): each slab gets the 30-day median for its exact grade (else the last sale), with history following the raw card's trend scaled to that price. A grade with no recent sales stays unpriced so you can set your own value. Scans ask for include=ebay, so a scanned slab is priced at its label's grade. The full graded price table and "What's it worth?" fall back to the typical grade curve, always labeled as an estimate, until a price guide like PriceCharting is connected.
Worth grading (/grading) asks for eBay summaries for every real card on the page in one call (GET /api/tcg/ebay?ids=…, backed by GET /v1/bulk/prices?include=ebay: 1 credit plus 1 per card with sales, up to 500 cards), so its PSA 10 and PSA 9 figures and the "sells steadily / hard to sell" labels come from real sales. A grade with no sales falls back to the curve and is marked "≈ estimated".
"What's it worth?" prices played conditions from TCGplayer's per-condition listings (/api/tcg/products/:id/conditions, GET /v1/cards/:id/prices/conditions): the listing median when there are 3+ listings, else the lowest one. Without those it uses eBay sales in that condition, then a labeled share of Near Mint. Both new routes are cached for 12 hours like the rest.
/api/cron/sync-prices)product_prices and today's price_snapshots (unknown prices are never written).portfolio_snapshots from stored data.profiles, user_preferences, products (catalog cache), product_prices (latest), price_snapshots (daily history), holdings (product + printing + condition + grade), transactions (append-only: purchase / sale / adjustment / correction), purchase_lots (view over purchase transactions; remaining quantity derived by FIFO replay so it can't drift), portfolio_snapshots, watchlist_items. RLS restricts every user table to auth.uid(); the shared catalog is read-only to clients and written by the server with the service role.
ios/ holds a native SwiftUI app that uses the same Supabase account and collection, and calls this app's API with the user's login token (getRequestUser accepts either the website's cookie or Authorization: Bearer). See ios/README.md for building in Xcode and shipping to TestFlight. .github/workflows/ios.yml compiles and tests it on a GitHub-hosted Mac.
src/app routes: overview, collection, product/[id], analytics, watchlist, activity, settings, login, api/*
src/components/ui shadcn-style primitives (Button, Dialog, Select, Segmented, …)
src/components/flows add-to-collection search + purchase form, sale / adjust / correction dialogs
src/components/portfolio, product, charts, shell
src/lib/finance ledger (FIFO), price resolver, timeline & analytics + tests
src/lib/pricing provider interface, TCG API provider, cache
src/lib/store repositories (demo / local / Supabase) and the React store
src/lib/supabase clients, typed schema, mappers, loader
supabase/migrations schema + RLS
tcgapi SDK's typed endpoints. TCG API's price_change_* fields are treated as percentages (as in the SDK's top-movers example); verify against your account.images.pokemontcg.io. Live mode uses TCG API's image_url.