feat(#67): Add support for multiple pokedexes per user

This commit is contained in:
Josh Creek
2026-01-04 22:06:36 +00:00
parent 578df57a45
commit 19a571e315
28 changed files with 1401 additions and 215 deletions
+116 -26
View File
@@ -2,11 +2,13 @@ import { type PokedexEntry, type PokedexEntryDB } from '$lib/models/PokedexEntry
import { type CatchRecord, type CatchRecordDB } from '$lib/models/CatchRecord';
import { type CombinedData } from '$lib/models/CombinedData';
import type { SupabaseClient } from '@supabase/supabase-js';
import { getRegionalDexColumnName } from '$lib/utils/regionalDexMapping';
class CombinedDataRepository {
constructor(
private supabase: SupabaseClient,
private userId: string
private userId: string | null,
private pokedexId: string | null
) {}
// Transform Supabase data to match frontend expectations (minimal transformation)
@@ -31,7 +33,23 @@ class CombinedDataRepository {
box: entry.boxPlacementBox || 0,
row: entry.boxPlacementRow || 0,
column: entry.boxPlacementColumn || 0
}
},
kantoDexNumber: entry.kanto_dex_number ?? undefined,
johtoDexNumber: entry.johto_dex_number ?? undefined,
hoennDexNumber: entry.hoenn_dex_number ?? undefined,
sinnohDexNumber: entry.sinnoh_dex_number ?? undefined,
unovaBwDexNumber: entry.unova_bw_dex_number ?? undefined,
unovaB2w2DexNumber: entry.unova_b2w2_dex_number ?? undefined,
kalosCentralDexNumber: entry.kalos_central_dex_number ?? undefined,
kalosCoastalDexNumber: entry.kalos_coastal_dex_number ?? undefined,
kalosMountainDexNumber: entry.kalos_mountain_dex_number ?? undefined,
alolaSmDexNumber: entry.alola_sm_dex_number ?? undefined,
alolaUsumDexNumber: entry.alola_usum_dex_number ?? undefined,
galarDexNumber: entry.galar_dex_number ?? undefined,
galarIsleOfArmorDexNumber: entry.galar_isle_of_armor_dex_number ?? undefined,
galarCrownTundraDexNumber: entry.galar_crown_tundra_dex_number ?? undefined,
hisuiDexNumber: entry.hisui_dex_number ?? undefined,
paldeaDexNumber: entry.paldea_dex_number ?? undefined
};
}
@@ -40,6 +58,7 @@ class CombinedDataRepository {
_id: record.id,
userId: record.userId,
pokedexEntryId: record.pokedexEntryId.toString(),
pokedexId: record.pokedexId,
haveToEvolve: record.haveToEvolve,
caught: record.caught,
inHome: record.inHome,
@@ -69,17 +88,31 @@ class CombinedDataRepository {
query = query.contains('gamesToCatchIn', [game]);
}
// Order by box placement
if (enableForms) {
query = query
.order('boxPlacementFormsBox', { ascending: true })
.order('boxPlacementFormsRow', { ascending: true })
.order('boxPlacementFormsColumn', { ascending: true });
// Determine ordering strategy based on game filter
if (game) {
const regionalColumn = getRegionalDexColumnName(game);
if (regionalColumn) {
// Order by regional dex with national dex fallback for NULLs
query = query
.order(regionalColumn, { ascending: true, nullsFirst: false })
.order('pokedexNumber', { ascending: true });
} else {
// Game specified but has no regional dex mapping
query = query.order('pokedexNumber', { ascending: true });
}
} else {
query = query
.order('boxPlacementBox', { ascending: true })
.order('boxPlacementRow', { ascending: true })
.order('boxPlacementColumn', { ascending: true });
// No game filter - use pre-calculated box placement (national dex order)
if (enableForms) {
query = query
.order('boxPlacementFormsBox', { ascending: true })
.order('boxPlacementFormsRow', { ascending: true })
.order('boxPlacementFormsColumn', { ascending: true });
} else {
query = query
.order('boxPlacementBox', { ascending: true })
.order('boxPlacementRow', { ascending: true })
.order('boxPlacementColumn', { ascending: true });
}
}
const { data: entries, error: entriesError } = await query;
@@ -95,12 +128,13 @@ class CombinedDataRepository {
// Get catch records for all entries
let catchRecords: CatchRecordDB[] = [];
if (userId) {
if (userId && this.pokedexId) {
const entryIds = entries.map((entry) => entry.id);
const { data: records, error: recordsError } = await this.supabase
.from('catch_records')
.select('*')
.eq('userId', userId)
.eq('pokedexId', this.pokedexId)
.in('pokedexEntryId', entryIds);
if (!recordsError && records) {
@@ -109,7 +143,7 @@ class CombinedDataRepository {
}
// Combine the data exactly like master branch
return entries.map((entry) => {
const combinedData = entries.map((entry) => {
const userCatchRecord =
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
@@ -123,6 +157,12 @@ class CombinedDataRepository {
catchRecord: transformedCatchRecord
};
});
// Recalculate box placement if using regional dex ordering
if (game && getRegionalDexColumnName(game)) {
return this.recalculateBoxPlacement(combinedData, enableForms);
}
return combinedData;
}
async findCombinedData(
@@ -151,17 +191,31 @@ class CombinedDataRepository {
query = query.contains('gamesToCatchIn', [game]);
}
// Order by box placement
if (enableForms) {
query = query
.order('boxPlacementFormsBox', { ascending: true })
.order('boxPlacementFormsRow', { ascending: true })
.order('boxPlacementFormsColumn', { ascending: true });
// Determine ordering strategy based on game filter
if (game) {
const regionalColumn = getRegionalDexColumnName(game);
if (regionalColumn) {
// Order by regional dex with national dex fallback for NULLs
query = query
.order(regionalColumn, { ascending: true, nullsFirst: false })
.order('pokedexNumber', { ascending: true });
} else {
// Game specified but has no regional dex mapping
query = query.order('pokedexNumber', { ascending: true });
}
} else {
query = query
.order('boxPlacementBox', { ascending: true })
.order('boxPlacementRow', { ascending: true })
.order('boxPlacementColumn', { ascending: true });
// No game filter - use pre-calculated box placement (national dex order)
if (enableForms) {
query = query
.order('boxPlacementFormsBox', { ascending: true })
.order('boxPlacementFormsRow', { ascending: true })
.order('boxPlacementFormsColumn', { ascending: true });
} else {
query = query
.order('boxPlacementBox', { ascending: true })
.order('boxPlacementRow', { ascending: true })
.order('boxPlacementColumn', { ascending: true });
}
}
const { data: entries, error: entriesError } = await query;
@@ -177,12 +231,13 @@ class CombinedDataRepository {
// Get catch records for these entries
let catchRecords: CatchRecordDB[] = [];
if (userId) {
if (userId && this.pokedexId) {
const entryIds = entries.map((entry) => entry.id);
const { data: records, error: recordsError } = await this.supabase
.from('catch_records')
.select('*')
.eq('userId', userId)
.eq('pokedexId', this.pokedexId)
.in('pokedexEntryId', entryIds);
if (!recordsError && records) {
@@ -191,7 +246,7 @@ class CombinedDataRepository {
}
// Combine the data exactly like master branch
return entries.map((entry) => {
const combinedData = entries.map((entry) => {
const userCatchRecord =
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
@@ -205,6 +260,12 @@ class CombinedDataRepository {
catchRecord: transformedCatchRecord
};
});
// Recalculate box placement if using regional dex ordering
if (game && getRegionalDexColumnName(game)) {
return this.recalculateBoxPlacement(combinedData, enableForms);
}
return combinedData;
}
async countCombinedData(
@@ -236,6 +297,35 @@ class CombinedDataRepository {
return count || 0;
}
/**
* Recalculates box placement based on the current order of data.
* Used when ordering by regional dex instead of pre-calculated national dex placement.
*/
private recalculateBoxPlacement(
data: CombinedData[],
useFormsPlacement: boolean
): CombinedData[] {
return data.map((item, index) => {
const placementIndex = index + 1; // 1-based indexing
const box = Math.ceil(placementIndex / 30);
const rowColumnIndex = (placementIndex - 1) % 30;
const row = Math.floor(rowColumnIndex / 6) + 1;
const column = (rowColumnIndex % 6) + 1;
const newPlacement = { box, row, column };
return {
...item,
pokedexEntry: {
...item.pokedexEntry,
...(useFormsPlacement
? { boxPlacementForms: newPlacement }
: { boxPlacement: newPlacement })
}
};
});
}
}
export default CombinedDataRepository;