Building PocketDex Tracker: A Next.js and Supabase App for Pokemon TCG Pocket Collections

Building PocketDex Tracker: A Next.js and Supabase App for Pokemon TCG Pocket Collections

构建 PocketDex Tracker:一款基于 Next.js 和 Supabase 的宝可梦卡牌(TCG Pocket)收藏追踪应用

PocketDex Tracker is a collection tracking app for Pokemon TCG Pocket. It lets players record which cards they own, monitor completion progress by set, search across the card database, and compare packs based on the cards they are still missing. The app is built with Next.js App Router, React 19, Supabase Auth, Supabase Postgres, row-level security, generated TypeScript database types, Tailwind CSS, shadcn-style UI components, and a small expected-value engine for pack recommendations.

PocketDex Tracker 是一款专为《宝可梦 TCG Pocket》设计的收藏追踪应用。它允许玩家记录自己拥有的卡牌、监控各卡包的收集进度、搜索卡牌数据库,并根据缺失的卡牌对比不同卡包的抽取价值。该应用基于 Next.js App Router、React 19、Supabase Auth、Supabase Postgres 构建,并利用了行级安全性(RLS)、自动生成的 TypeScript 数据库类型、Tailwind CSS、shadcn 风格的 UI 组件,以及一个用于卡包推荐的小型期望值引擎。

项目链接

Live app: pocketdex-tracker.vercel.app GitHub repo: mwiginton/pocketdex-tracker

在线应用:pocketdex-tracker.vercel.app GitHub 仓库:mwiginton/pocketdex-tracker

What the App Does

应用功能

PocketDex Tracker supports the core workflows of managing a digital card collection:

  • Sign in with email and password.
  • View overall collection completion.
  • Drill into individual sets.
  • Mark cards as owned or missing.
  • Search cards by name, set, rarity, type, and ownership status.
  • Compare pack recommendations using missing card pull odds.
  • Export or import collection data as JSON or CSV.

PocketDex Tracker 支持管理数字卡牌收藏的核心工作流:

  • 通过邮箱和密码登录。
  • 查看整体收藏完成度。
  • 深入查看各个卡包系列。
  • 标记卡牌为“已拥有”或“缺失”。
  • 按名称、系列、稀有度、类型和拥有状态搜索卡牌。
  • 利用缺失卡牌的抽取概率对比卡包推荐。
  • 以 JSON 或 CSV 格式导出或导入收藏数据。

At a high level, the app combines static card metadata with each user’s private ownership state. That combination powers both progress tracking and pack ranking.

从宏观层面来看,该应用将静态的卡牌元数据与每个用户的私有拥有状态相结合。这种结合驱动了进度追踪和卡包排名功能。

Tech Stack

技术栈

The frontend uses Next.js App Router with a mix of Server Components and Client Components. Server Components handle authenticated data loading, while Client Components handle interactive ownership toggles and optimistic UI updates.

前端使用 Next.js App Router,并混合了服务端组件(Server Components)和客户端组件(Client Components)。服务端组件处理已认证的数据加载,而客户端组件则处理交互式的拥有状态切换和乐观 UI 更新。

Supabase provides the backend:

  • Supabase Auth manages users.
  • Postgres stores sets, cards, packs, pull odds, and owned-card records.
  • Row-level security protects per-user collection data.
  • SQL RPC functions return dashboard aggregates and recommendation rows.
  • Generated TypeScript types make database access safer.

Supabase 提供后端支持:

  • Supabase Auth 管理用户。
  • Postgres 存储系列、卡牌、卡包、抽取概率和拥有卡牌记录。
  • 行级安全性(RLS)保护每个用户的收藏数据。
  • SQL RPC 函数返回仪表板聚合数据和推荐行。
  • 自动生成的 TypeScript 类型使数据库访问更安全。

The interface is styled with Tailwind CSS, shadcn-style primitives, Radix UI components, Lucide icons, and Next Image for card artwork.

界面采用 Tailwind CSS、shadcn 风格的原语、Radix UI 组件、Lucide 图标进行样式设计,并使用 Next Image 展示卡牌原画。

Data Model

数据模型

The database is organized around five main tables:

  • sets: expansion metadata such as name, release date, card counts, and images.
  • cards: individual cards, collector numbers, rarity, category, type, and image URLs.
  • packs: booster packs tied to sets.
  • card_pack_odds: pull probabilities for each card in each pack.
  • user_cards: the signed-in user’s owned-card state.

数据库围绕五个主要表组织:

  • sets:扩展包元数据,如名称、发布日期、卡牌数量和图片。
  • cards:单张卡牌信息、收藏编号、稀有度、类别、类型和图片 URL。
  • packs:与系列绑定的补充包。
  • card_pack_odds:每张卡牌在每个卡包中的抽取概率。
  • user_cards:已登录用户的拥有卡牌状态。

Most tables are public read data. The sensitive table is user_cards, where RLS policies ensure users can only read, insert, update, or delete their own rows. That separation keeps shared card data simple while preserving privacy for user-specific collection state.

大多数表为公开读取数据。敏感表是 user_cards,其中 RLS 策略确保用户只能读取、插入、更新或删除自己的行。这种分离方式既保持了共享卡牌数据的简洁性,又保护了用户特定收藏状态的隐私。

Authentication and Sessions

身份验证与会话

The app uses @supabase/ssr to create separate Supabase clients for server and browser environments. Protected pages call supabase.auth.getUser() on the server. If there is no authenticated user, the app redirects to /auth/login. A Next.js proxy refreshes Supabase sessions across requests, allowing Server Components to load user-specific data without moving authentication checks entirely into client code.

该应用使用 @supabase/ssr 为服务端和浏览器环境创建独立的 Supabase 客户端。受保护页面会在服务端调用 supabase.auth.getUser()。如果没有已认证用户,应用会重定向至 /auth/login。Next.js 代理会在请求间刷新 Supabase 会话,允许服务端组件加载用户特定数据,而无需将身份验证检查完全移至客户端代码中。

Dashboard Data

仪表板数据

The home page shows overall completion, owned cards, missing cards, total cards, and progress by set. Instead of fetching every card and calculating completion in React, the app calls a Postgres RPC function: const { data, error } = await supabase.rpc("get_home_set_completion_rows");

主页显示整体完成度、已拥有卡牌、缺失卡牌、卡牌总数以及各系列的进度。应用没有在 React 中获取所有卡牌并进行计算,而是调用了一个 Postgres RPC 函数:const { data, error } = await supabase.rpc("get_home_set_completion_rows");

The UI then derives:

  • overall completion ratio
  • per-set completion ratio
  • closest set to finishing

UI 随后推导出:

  • 整体完成比例
  • 各系列完成比例
  • 最接近完成的系列

This keeps aggregate work close to the database and lets the page render from compact, purpose-built result rows.

这使得聚合工作保持在数据库端,并允许页面通过紧凑的、专用的结果行进行渲染。

Tracking Cards

卡牌追踪

Set pages load card metadata on the server and pass it into a client-side CollectionGrid. The grid manages local state for owned cards, pending saves, errors, and filters. When a user marks a card as owned, the UI updates optimistically and then writes to Supabase: await supabase.from("user_cards").upsert({ user_id: userId, card_id: cardId });

系列页面在服务端加载卡牌元数据,并将其传递给客户端的 CollectionGrid。该网格管理已拥有卡牌、待保存状态、错误和过滤器的本地状态。当用户将一张卡牌标记为已拥有时,UI 会进行乐观更新,然后写入 Supabase:await supabase.from("user_cards").upsert({ user_id: userId, card_id: cardId });

Unmarking a card deletes the matching user_cards row. The same interaction pattern appears in search results, so users can update their collection from either the set view or the search view.

取消标记一张卡牌会删除对应的 user_cards 行。同样的交互模式也出现在搜索结果中,因此用户可以从系列视图或搜索视图更新他们的收藏。

Search and Filtering

搜索与过滤

The search page supports filters for:

  • card name
  • set
  • rarity
  • Pokemon type or trainer category
  • owned or missing status

搜索页面支持以下过滤条件:

  • 卡牌名称
  • 系列
  • 稀有度
  • 宝可梦类型或训练家类别
  • 已拥有或缺失状态

Search is implemented with Supabase queries and URL search params, making filtered views reload-safe and easy to share. For owned-only searches, the app uses an inner join against user_cards. For missing-card searches, it checks for null joined ownership rows.

搜索功能通过 Supabase 查询和 URL 搜索参数实现,使得过滤后的视图在刷新后依然有效且易于分享。对于“仅显示已拥有”的搜索,应用使用针对 user_cards 的内连接(inner join)。对于“缺失卡牌”的搜索,它会检查连接后的拥有状态行是否为空。

Pack Recommendations

卡包推荐

Pack recommendations are based on the user’s missing cards and the pull probabilities for each pack. The app asks Supabase for rows containing: recommendable packs, missing cards, pull probabilities, and card metadata.

卡包推荐基于用户缺失的卡牌以及每个卡包的抽取概率。应用向 Supabase 请求包含以下内容的行:可推荐的卡包、缺失的卡牌、抽取概率和卡牌元数据。

The TypeScript recommendation logic groups those rows by pack and calculates expected new cards: expectedNewCards += pullProbability;

TypeScript 推荐逻辑按卡包对这些行进行分组,并计算预期获得的新卡牌:expectedNewCards += pullProbability;

For each pack, the app sums the probability of pulling each missing card. The final list is sorted by expected value, then by pack order and name for stable ranking. Recommendations can be scoped to the whole collection or to a specific set. The app also supports an option to include unavailable limited-time packs, stored as an HTTP-only cookie.

对于每个卡包,应用会汇总抽取到每张缺失卡牌的概率。最终列表按期望值排序,随后按卡包顺序和名称进行稳定排名。推荐范围可以设定为整个收藏或特定系列。应用还支持包含不可用的限时卡包选项,该选项存储在仅 HTTP 的 Cookie 中。

Import and Export

导入与导出

The settings page gives users a way to back up and move their collection data. Exports are available as JSON or CSV and include card IDs, ownership dates, and card metadata. Imports accept PocketDex JSON or CSV files, validate card IDs against the database, skip unknown rows, ignore duplicates, and upsert valid owned-card rows in batches.

设置页面为用户提供了备份和迁移收藏数据的方法。导出格式支持 JSON 或 CSV,包含卡牌 ID、拥有日期和卡牌元数据。导入功能接受 PocketDex 的 JSON 或 CSV 文件,验证数据库中的卡牌 ID,跳过未知行,忽略重复项,并批量更新(upsert)有效的已拥有卡牌行。

Testing the Recommendation Logic

测试推荐逻辑

The recommendation logic lives in lib/recommendation, separate from the UI. It can be compiled and tested independently with Node’s built-in test runner. That keeps the expected-value calculation easy to verify: given packs, missing cards, and odds, the same inputs should always produce the same rankings.

推荐逻辑位于 lib/recommendation 中,与 UI 分离。它可以使用 Node 内置的测试运行器进行独立编译和测试。这使得期望值计算易于验证:给定卡包、缺失卡牌和概率,相同的输入应始终产生相同的排名。

Running Locally

本地运行

To run the app:

npm install
cp .env.example .env.local
npm run dev

Then configure the Supabase URL and publishable key in .env.local. The database setup lives in the supabase/ directory.

运行应用:

npm install
cp .env.example .env.local
npm run dev

然后在 .env.local 中配置 Supabase URL 和可发布密钥。数据库设置位于 supabase/ 目录中。