<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michelle Wiginton</title>
    <description>The latest articles on DEV Community by Michelle Wiginton (@mwiginton).</description>
    <link>https://dev.to/mwiginton</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3354908%2F1fb3ea40-4ee1-4c0c-908e-2481b3e62632.jpg</url>
      <title>DEV Community: Michelle Wiginton</title>
      <link>https://dev.to/mwiginton</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mwiginton"/>
    <language>en</language>
    <item>
      <title>Building PocketDex Tracker: A Next.js and Supabase App for Pokemon TCG Pocket Collections</title>
      <dc:creator>Michelle Wiginton</dc:creator>
      <pubDate>Sun, 28 Jun 2026 22:04:37 +0000</pubDate>
      <link>https://dev.to/mwiginton/building-pocketdex-tracker-a-nextjs-and-supabase-app-for-pokemon-tcg-pocket-collections-3197</link>
      <guid>https://dev.to/mwiginton/building-pocketdex-tracker-a-nextjs-and-supabase-app-for-pokemon-tcg-pocket-collections-3197</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live app: &lt;a href="https://pocketdex-tracker.vercel.app/" rel="noopener noreferrer"&gt;pocketdex-tracker.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub repo: &lt;a href="https://github.com/mwiginton/pocketdex-tracker" rel="noopener noreferrer"&gt;mwiginton/pocketdex-tracker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the App Does
&lt;/h2&gt;

&lt;p&gt;PocketDex Tracker supports the core workflows of managing a digital card collection:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Supabase provides the backend:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Data Model
&lt;/h2&gt;

&lt;p&gt;The database is organized around five main tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sets&lt;/code&gt;: expansion metadata such as name, release date, card counts, and images.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cards&lt;/code&gt;: individual cards, collector numbers, rarity, category, type, and image URLs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packs&lt;/code&gt;: booster packs tied to sets.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;card_pack_odds&lt;/code&gt;: pull probabilities for each card in each pack.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_cards&lt;/code&gt;: the signed-in user's owned-card state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most tables are public read data. The sensitive table is &lt;code&gt;user_cards&lt;/code&gt;, where RLS policies ensure users can only read, insert, update, or delete their own rows.&lt;/p&gt;

&lt;p&gt;That separation keeps shared card data simple while preserving privacy for user-specific collection state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication and Sessions
&lt;/h2&gt;

&lt;p&gt;The app uses &lt;code&gt;@supabase/ssr&lt;/code&gt; to create separate Supabase clients for server and browser environments.&lt;/p&gt;

&lt;p&gt;Protected pages call &lt;code&gt;supabase.auth.getUser()&lt;/code&gt; on the server. If there is no authenticated user, the app redirects to &lt;code&gt;/auth/login&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboard Data
&lt;/h2&gt;

&lt;p&gt;The home page shows overall completion, owned cards, missing cards, total cards, and progress by set.&lt;/p&gt;

&lt;p&gt;Instead of fetching every card and calculating completion in React, the app calls a Postgres RPC function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_home_set_completion_rows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI then derives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overall completion ratio&lt;/li&gt;
&lt;li&gt;per-set completion ratio&lt;/li&gt;
&lt;li&gt;closest set to finishing
This keeps aggregate work close to the database and lets the page render from compact, purpose-built result rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tracking Cards
&lt;/h2&gt;

&lt;p&gt;Set pages load card metadata on the server and pass it into a client-side CollectionGrid.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_cards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;card_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unmarking a card deletes the matching user_cards row.&lt;/p&gt;

&lt;p&gt;The same interaction pattern appears in search results, so users can update their collection from either the set view or the search view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search and Filtering
&lt;/h2&gt;

&lt;p&gt;The search page supports filters for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;card name&lt;/li&gt;
&lt;li&gt;set&lt;/li&gt;
&lt;li&gt;rarity&lt;/li&gt;
&lt;li&gt;Pokemon type or trainer category&lt;/li&gt;
&lt;li&gt;owned or missing status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Search is implemented with Supabase queries and URL search params, making filtered views reload-safe and easy to share.&lt;/p&gt;

&lt;p&gt;For owned-only searches, the app uses an inner join against &lt;code&gt;user_cards&lt;/code&gt;. For missing-card searches, it checks for null joined ownership rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pack Recommendations
&lt;/h2&gt;

&lt;p&gt;Pack recommendations are based on the user’s missing cards and the pull probabilities for each pack.&lt;br&gt;
The app asks Supabase for rows containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recommendable packs&lt;/li&gt;
&lt;li&gt;missing cards&lt;/li&gt;
&lt;li&gt;pull probabilities&lt;/li&gt;
&lt;li&gt;card metadata
The TypeScript recommendation logic groups those rows by pack and calculates expected new cards:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;expectedNewCards&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;pullProbability&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Import and Export
&lt;/h2&gt;

&lt;p&gt;The settings page gives users a way to back up and move their collection data.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing the Recommendation Logic
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That keeps the expected-value calculation easy to verify: given packs, missing cards, and odds, the same inputs should always produce the same rankings.&lt;/p&gt;
&lt;h2&gt;
  
  
  Running Locally
&lt;/h2&gt;

&lt;p&gt;To run the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt;
&lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;
&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="nx"&gt;dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure the Supabase URL and publishable key in .env.local.&lt;/p&gt;

&lt;p&gt;The database setup lives in the supabase/ directory, including base schema, RLS policies, RPC functions, and seed files for supported Pokemon TCG Pocket sets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;PocketDex Tracker uses server-rendered data where possible, client interactivity where it improves the experience, and database-side logic for aggregation-heavy queries. The result is a compact full-stack app with typed data access, RLS-protected user state, optimistic updates, SQL-backed summaries, portable collection data, and recommendation logic that can be tested independently from the interface.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>postgres</category>
      <category>javascript</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
