-
-
Legal Disclaimer
-
- Living Dex Tracker is a fan-made project. We do not claim ownership of any Pokémon
- characters, images, or other content featured on this website. This project is not
- affiliated with, endorsed, sponsored, or specifically approved by Nintendo, Game Freak, or
- The Pokémon Company.
-
+
+
+
+
+
+
Pokémon caught
+
{pokemonCaught}
+
+
+
+
Living Dexes Completed
+
{livingDexesCompleted}
+
-
+
+
+
+
+
+
+
What is a Living Dex?
+
+ A Living Dex is a complete Pokédex where you keep one of every Pokémon in your boxes
+ (often including forms/variants). Living Dex Tracker helps you build and maintain that
+ collection with filters, notes, and progress tracking.
+
+
+
+
+
+
+
+
+
+
Why Choose Living Dex Tracker?
+
+
+
+
+
+
+
+
Social & Shareable
+
+ Easily share your Pokédex journey with friends, or find theirs. If you'd rather go
+ it alone, that's okay too!
+
+
+
+
+
+
+
+
+
+
+
+
Free & Open Source
+
+ Completely open source and free to use, enabling the community to contribute
+ updates as soon as new Pokémon are released.
+
+
+
+
+
+
+
+
+
+
+
+
Advanced Filtering
+
+ Track simple progress or tackle harder variants like a Living Origin Form Dex with
+ our powerful filtering options for targeted catching sessions.
+
+
+
+
+
+
+
+
+
+
+
+
100% free
+
+ Did we mention it's completely free to use? Oh, we did? Good. Because it is.
+
+
+
+
+
+
+
+
+
+
+
+
+
Ready to Start Your Journey?
+
+ Get started today with tracking your Living Pokédex progress. It's free, open source, and
+ built with love for the Pokémon community.
+
+
+
+
+
+
+
+
+
+
+
Legal Disclaimer
+
+ Living Dex Tracker is a fan-made project. We do not claim ownership of any Pokémon
+ characters, images, or other content featured on this website. This project is not
+ affiliated with, endorsed, sponsored, or specifically approved by Nintendo, Game Freak,
+ or The Pokémon Company.
+
+
+
+
+
+{/if}
diff --git a/src/routes/api/pokedexes/+server.ts b/src/routes/api/pokedexes/+server.ts
index cf3758b..4da429e 100644
--- a/src/routes/api/pokedexes/+server.ts
+++ b/src/routes/api/pokedexes/+server.ts
@@ -8,6 +8,7 @@ import type { SupabaseClient } from '@supabase/supabase-js';
/**
* Calculate expected pokedex entries based on pokedex configuration
* Applies the same filtering logic as CombinedDataRepository
+ * @throws Error if the Supabase query fails
*/
async function calculateExpectedEntries(
supabase: SupabaseClient,
@@ -28,7 +29,7 @@ async function calculateExpectedEntries(
.from('region_game_mappings')
.select('region')
.eq('game', pokedex.gameScope)
- .single();
+ .maybeSingle();
if (regionData?.region) {
query = query.eq('regionToCatchIn', regionData.region);
@@ -44,7 +45,7 @@ async function calculateExpectedEntries(
if (error) {
console.error('Error calculating expected entries:', error);
- return [];
+ throw new Error(`Failed to calculate expected entries: ${error.message}`);
}
return data?.map((entry) => entry.id) || [];
@@ -52,6 +53,8 @@ async function calculateExpectedEntries(
/**
* Populate pokedex_entries_mapping table for a pokedex
+ * Uses chunked upsert to avoid UNIQUE constraint violations and request size limits
+ * @throws Error if the Supabase upsert fails
*/
async function populatePokedexMappings(
supabase: SupabaseClient,
@@ -70,11 +73,19 @@ async function populatePokedexMappings(
pokedexEntryId
}));
- const { error } = await supabase.from('pokedex_entries_mapping').insert(mappings);
+ // Process in chunks to avoid request size limits
+ const CHUNK_SIZE = 500;
+ for (let i = 0; i < mappings.length; i += CHUNK_SIZE) {
+ const chunk = mappings.slice(i, i + CHUNK_SIZE);
+ const { error } = await supabase.from('pokedex_entries_mapping').upsert(chunk, {
+ onConflict: 'pokedexId,pokedexEntryId',
+ ignoreDuplicates: true
+ });
- if (error) {
- console.error('Error populating pokedex mappings:', error);
- throw new Error(`Failed to populate pokedex mappings: ${error.message}`);
+ if (error) {
+ console.error('Error populating pokedex mappings:', error);
+ throw new Error(`Failed to populate pokedex mappings: ${error.message}`);
+ }
}
}
diff --git a/src/routes/api/pokedexes/[id]/+server.ts b/src/routes/api/pokedexes/[id]/+server.ts
index 41b80bf..5ce94d0 100644
--- a/src/routes/api/pokedexes/[id]/+server.ts
+++ b/src/routes/api/pokedexes/[id]/+server.ts
@@ -8,6 +8,7 @@ import type { SupabaseClient } from '@supabase/supabase-js';
/**
* Calculate expected pokedex entries based on pokedex configuration
* Applies the same filtering logic as CombinedDataRepository
+ * @throws Error if the Supabase query fails
*/
async function calculateExpectedEntries(
supabase: SupabaseClient,
@@ -28,7 +29,7 @@ async function calculateExpectedEntries(
.from('region_game_mappings')
.select('region')
.eq('game', pokedex.gameScope)
- .single();
+ .maybeSingle();
if (regionData?.region) {
query = query.eq('regionToCatchIn', regionData.region);
@@ -44,16 +45,17 @@ async function calculateExpectedEntries(
if (error) {
console.error('Error calculating expected entries:', error);
- return [];
+ throw new Error(`Failed to calculate expected entries: ${error.message}`);
}
return data?.map((entry) => entry.id) || [];
}
/**
- * Populate pokedex_entries_mapping table for a pokedex
+ * Recalculate pokedex_entries_mapping table for a pokedex (used on update)
+ * Uses a single atomic RPC call to prevent orphaned pokedexes
*/
-async function populatePokedexMappings(
+async function recalculatePokedexMappings(
supabase: SupabaseClient,
pokedexId: string,
pokedex: Pokedex
@@ -65,42 +67,18 @@ async function populatePokedexMappings(
return;
}
- const mappings = expectedEntryIds.map((pokedexEntryId) => ({
- pokedexId,
- pokedexEntryId
- }));
-
- const { error } = await supabase.from('pokedex_entries_mapping').insert(mappings);
+ // Call the atomic RPC function to delete old mappings and insert new ones
+ const { error } = await supabase.rpc('recalculate_pokedex_mappings', {
+ p_pokedex_id: pokedexId,
+ p_entry_ids: expectedEntryIds
+ });
if (error) {
- console.error('Error populating pokedex mappings:', error);
- throw new Error(`Failed to populate pokedex mappings: ${error.message}`);
+ console.error('Error recalculating pokedex mappings:', error);
+ throw new Error(`Failed to recalculate pokedex mappings: ${error.message}`);
}
}
-/**
- * Recalculate pokedex_entries_mapping table for a pokedex (used on update)
- */
-async function recalculatePokedexMappings(
- supabase: SupabaseClient,
- pokedexId: string,
- pokedex: Pokedex
-): Promise
{
- // Delete old mappings
- const { error: deleteError } = await supabase
- .from('pokedex_entries_mapping')
- .delete()
- .eq('pokedexId', pokedexId);
-
- if (deleteError) {
- console.error('Error deleting old pokedex mappings:', deleteError);
- throw new Error(`Failed to delete old pokedex mappings: ${deleteError.message}`);
- }
-
- // Populate new mappings
- await populatePokedexMappings(supabase, pokedexId, pokedex);
-}
-
// GET: Get single pokedex by ID
export const GET = async (event: RequestEvent) => {
try {
@@ -154,6 +132,13 @@ export const PUT = async (event: RequestEvent) => {
}
const repo = new PokedexRepository(event.locals.supabase, userId);
+
+ // Fetch the existing pokedex before update to compare values
+ const existingPokedex = await repo.findById(id);
+ if (!existingPokedex) {
+ return json({ error: 'Pokedex not found' }, { status: 404 });
+ }
+
const pokedex = await repo.update(id, data);
if (!pokedex) {
@@ -161,16 +146,16 @@ export const PUT = async (event: RequestEvent) => {
}
// Recalculate pokedex_entries_mapping if configuration changed
- // Only recalculate if type flags or gameScope changed
+ // Only recalculate if type flags or gameScope actually changed
const configChanged =
- data.isFormDex !== undefined ||
- data.gameScope !== undefined ||
- (data.isLivingDex !== undefined && pokedex.isLivingDex !== data.isLivingDex) ||
- (data.isShinyDex !== undefined && pokedex.isShinyDex !== data.isShinyDex) ||
- (data.isOriginDex !== undefined && pokedex.isOriginDex !== data.isOriginDex);
+ (data.isFormDex !== undefined && existingPokedex.isFormDex !== data.isFormDex) ||
+ (data.gameScope !== undefined && existingPokedex.gameScope !== data.gameScope) ||
+ (data.isLivingDex !== undefined && existingPokedex.isLivingDex !== data.isLivingDex) ||
+ (data.isShinyDex !== undefined && existingPokedex.isShinyDex !== data.isShinyDex) ||
+ (data.isOriginDex !== undefined && existingPokedex.isOriginDex !== data.isOriginDex);
if (configChanged) {
- await recalculatePokedexMappings(event.locals.supabase, id, { ...pokedex, ...data });
+ await recalculatePokedexMappings(event.locals.supabase, id, pokedex);
}
return json(pokedex);
diff --git a/src/routes/my-pokedexes/+page.svelte b/src/routes/my-pokedexes/+page.svelte
index 9677d5f..8eed695 100644
--- a/src/routes/my-pokedexes/+page.svelte
+++ b/src/routes/my-pokedexes/+page.svelte
@@ -1,5 +1,5 @@