diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index ad63a7c..5ecdc77 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -24,6 +24,19 @@ onMount(async () => { await getUser(); + // Listen for auth state changes to keep the user store in sync + const { + data: { subscription } + } = supabase.auth.onAuthStateChange((event, session) => { + console.log('[Layout] Auth state changed:', event, session ? 'Session found' : 'No session'); + if (session) { + localUser = session.user; + } else { + localUser = null; + } + user.set(localUser); + }); + if (pwaInfo) { const { registerSW } = await import('virtual:pwa-register'); registerSW({ @@ -41,6 +54,10 @@ } }); } + + return () => { + subscription.unsubscribe(); + }; }); async function getUser() { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f5f1d0b..2c74681 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -15,6 +15,8 @@ }); onDestroy(unsubscribe); + let isCheckingSession = true; + onMount(() => { checkSessionAndRedirect(); }); @@ -30,6 +32,8 @@ } } catch (error) { console.error('Error checking session:', error); + } finally { + isCheckingSession = false; } } @@ -67,304 +71,322 @@ /> - -
-
-
-
-
- - - - Free -
-
- - - - Open Source -
-
- - - - Offline-friendly -
-
-

Start Your Pokédex Journey!

-

- Track your progress towards a complete Living Pokédex — one of every Pokémon, actively - maintained across your boxes. Join thousands of trainers worldwide. -

- -
-
-
-

Get Started Free

- -
+{#if isCheckingSession} + +
+
+
-
- - -
-
-
-
-
Pokémon caught
-
{pokemonCaught}
-
-
-
Users
-
{users}
-
-
-
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. +{:else} + +

+
+
+
+
+ + Free +
+
+ + Open Source +
+
+ + Offline-friendly +
+
+

Start Your Pokédex Journey!

+

+ Track your progress towards a complete Living Pokédex — one of every Pokémon, actively + maintained across your boxes. Join thousands of trainers worldwide.

-
-
-
-
- - -
-
-

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. -

-
-

Sign Up Now

+

Get Started Free

-
- -
-
-
-
-

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}
+
+
+
Users
+
{users}
+
+
+
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. +

+
+
+
+

Sign Up Now

+ +
+
+
+
+
+ + +
+
+
+
+

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 @@