mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-15 03:53:43 +00:00
feat(#67): Add support for multiple pokedexes per user
This commit is contained in:
@@ -4,7 +4,8 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
class CatchRecordRepository {
|
||||
constructor(
|
||||
private supabase: SupabaseClient,
|
||||
private userId: string
|
||||
private userId: string,
|
||||
private pokedexId: string
|
||||
) {}
|
||||
|
||||
// Transform Supabase data to frontend format (minimal transformation)
|
||||
@@ -13,6 +14,7 @@ class CatchRecordRepository {
|
||||
_id: record.id,
|
||||
userId: record.userId,
|
||||
pokedexEntryId: record.pokedexEntryId.toString(),
|
||||
pokedexId: record.pokedexId,
|
||||
haveToEvolve: record.haveToEvolve,
|
||||
caught: record.caught,
|
||||
inHome: record.inHome,
|
||||
@@ -31,6 +33,7 @@ class CatchRecordRepository {
|
||||
}
|
||||
dbData.pokedexEntryId = numericId;
|
||||
}
|
||||
if (data.pokedexId !== undefined) dbData.pokedexId = data.pokedexId;
|
||||
if (data.haveToEvolve !== undefined) dbData.haveToEvolve = data.haveToEvolve;
|
||||
if (data.caught !== undefined) dbData.caught = data.caught;
|
||||
if (data.inHome !== undefined) dbData.inHome = data.inHome;
|
||||
@@ -46,6 +49,7 @@ class CatchRecordRepository {
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.eq('userId', this.userId)
|
||||
.eq('pokedexId', this.pokedexId)
|
||||
.single();
|
||||
|
||||
if (error || !data) return null;
|
||||
@@ -56,7 +60,8 @@ class CatchRecordRepository {
|
||||
const { data, error } = await this.supabase
|
||||
.from('catch_records')
|
||||
.select('*')
|
||||
.eq('userId', this.userId);
|
||||
.eq('userId', this.userId)
|
||||
.eq('pokedexId', this.pokedexId);
|
||||
|
||||
if (error || !data) return [];
|
||||
return data.map((record) => this.transformCatchRecord(record));
|
||||
@@ -66,7 +71,8 @@ class CatchRecordRepository {
|
||||
const { data, error } = await this.supabase
|
||||
.from('catch_records')
|
||||
.select('*')
|
||||
.eq('userId', userId);
|
||||
.eq('userId', userId)
|
||||
.eq('pokedexId', this.pokedexId);
|
||||
|
||||
if (error || !data) return [];
|
||||
return data.map((record) => this.transformCatchRecord(record));
|
||||
@@ -75,6 +81,7 @@ class CatchRecordRepository {
|
||||
async create(data: Partial<CatchRecord>): Promise<CatchRecord> {
|
||||
const dbData = {
|
||||
userId: this.userId,
|
||||
pokedexId: this.pokedexId,
|
||||
...this.transformToDatabase(data)
|
||||
};
|
||||
|
||||
@@ -104,6 +111,7 @@ class CatchRecordRepository {
|
||||
.update(dbData)
|
||||
.eq('id', id)
|
||||
.eq('userId', this.userId)
|
||||
.eq('pokedexId', this.pokedexId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
@@ -116,7 +124,8 @@ class CatchRecordRepository {
|
||||
.from('catch_records')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.eq('userId', this.userId);
|
||||
.eq('userId', this.userId)
|
||||
.eq('pokedexId', this.pokedexId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting catch record:', error);
|
||||
@@ -128,8 +137,8 @@ class CatchRecordRepository {
|
||||
if (data._id) {
|
||||
return this.update(data._id, data);
|
||||
} else if (data.pokedexEntryId) {
|
||||
// Try to find existing record for this user and pokemon
|
||||
const existing = await this.findByUserAndPokemon(this.userId, data.pokedexEntryId);
|
||||
// Try to find existing record for this user, pokemon, and pokedex
|
||||
const existing = await this.findByUserAndPokemon(this.userId, data.pokedexEntryId, this.pokedexId);
|
||||
if (existing) {
|
||||
return this.update(existing._id, data);
|
||||
} else {
|
||||
@@ -139,7 +148,7 @@ class CatchRecordRepository {
|
||||
return this.create(data);
|
||||
}
|
||||
|
||||
async findByUserAndPokemon(userId: string, pokedexEntryId: string): Promise<CatchRecord | null> {
|
||||
async findByUserAndPokemon(userId: string, pokedexEntryId: string, pokedexId: string): Promise<CatchRecord | null> {
|
||||
const numericId = Number(pokedexEntryId);
|
||||
if (isNaN(numericId)) {
|
||||
return null;
|
||||
@@ -149,6 +158,7 @@ class CatchRecordRepository {
|
||||
.select('*')
|
||||
.eq('userId', userId)
|
||||
.eq('pokedexEntryId', numericId)
|
||||
.eq('pokedexId', pokedexId)
|
||||
.single();
|
||||
|
||||
if (error || !data) return null;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,7 +26,23 @@ class PokedexEntryRepository {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,6 +90,7 @@ class PokedexEntryRepository {
|
||||
if (data.boxPlacement?.row !== undefined) dbData.boxPlacementRow = data.boxPlacement.row;
|
||||
if (data.boxPlacement?.column !== undefined)
|
||||
dbData.boxPlacementColumn = data.boxPlacement.column;
|
||||
if (data.regionalDexNumbers !== undefined) dbData.regionalDexNumbers = data.regionalDexNumbers;
|
||||
|
||||
const { data: result, error } = await this.supabase
|
||||
.from('pokedex_entries')
|
||||
@@ -108,6 +125,7 @@ class PokedexEntryRepository {
|
||||
if (data.boxPlacement?.row !== undefined) dbData.boxPlacementRow = data.boxPlacement.row;
|
||||
if (data.boxPlacement?.column !== undefined)
|
||||
dbData.boxPlacementColumn = data.boxPlacement.column;
|
||||
if (data.regionalDexNumbers !== undefined) dbData.regionalDexNumbers = data.regionalDexNumbers;
|
||||
|
||||
const { data: result, error } = await this.supabase
|
||||
.from('pokedex_entries')
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Pokedex, PokedexDB } from '$lib/models/Pokedex';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
class PokedexRepository {
|
||||
constructor(
|
||||
private supabase: SupabaseClient,
|
||||
private userId: string
|
||||
) {}
|
||||
|
||||
private transform(db: PokedexDB): Pokedex {
|
||||
return {
|
||||
_id: db.id,
|
||||
userId: db.userId,
|
||||
name: db.name,
|
||||
description: db.description || '',
|
||||
isLivingDex: db.isLivingDex,
|
||||
isShinyDex: db.isShinyDex,
|
||||
isOriginDex: db.isOriginDex,
|
||||
isFormDex: db.isFormDex,
|
||||
gameScope: db.gameScope
|
||||
};
|
||||
}
|
||||
|
||||
private transformToDatabase(data: Partial<Pokedex>): Partial<PokedexDB> {
|
||||
const dbData: Partial<PokedexDB> = {};
|
||||
if (data.name !== undefined) dbData.name = data.name;
|
||||
if (data.description !== undefined) dbData.description = data.description;
|
||||
if (data.isLivingDex !== undefined) dbData.isLivingDex = data.isLivingDex;
|
||||
if (data.isShinyDex !== undefined) dbData.isShinyDex = data.isShinyDex;
|
||||
if (data.isOriginDex !== undefined) dbData.isOriginDex = data.isOriginDex;
|
||||
if (data.isFormDex !== undefined) dbData.isFormDex = data.isFormDex;
|
||||
if (data.gameScope !== undefined) dbData.gameScope = data.gameScope;
|
||||
return dbData;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Pokedex | null> {
|
||||
const { data, error } = await this.supabase
|
||||
.from('pokedexes')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.eq('userId', this.userId)
|
||||
.single();
|
||||
|
||||
if (error || !data) return null;
|
||||
return this.transform(data);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Pokedex[]> {
|
||||
const { data, error } = await this.supabase
|
||||
.from('pokedexes')
|
||||
.select('*')
|
||||
.eq('userId', this.userId)
|
||||
.order('createdAt', { ascending: true });
|
||||
|
||||
if (error || !data) return [];
|
||||
return data.map((d) => this.transform(d));
|
||||
}
|
||||
|
||||
async create(data: Partial<Pokedex>): Promise<Pokedex> {
|
||||
const dbData = {
|
||||
userId: this.userId,
|
||||
...this.transformToDatabase(data)
|
||||
};
|
||||
|
||||
const { data: result, error } = await this.supabase
|
||||
.from('pokedexes')
|
||||
.insert(dbData)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating pokedex:', error);
|
||||
throw new Error(`Failed to create pokedex: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Failed to create pokedex: No result returned');
|
||||
}
|
||||
|
||||
return this.transform(result);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<Pokedex>): Promise<Pokedex | null> {
|
||||
const dbData = this.transformToDatabase(data);
|
||||
|
||||
const { data: result, error } = await this.supabase
|
||||
.from('pokedexes')
|
||||
.update(dbData)
|
||||
.eq('id', id)
|
||||
.eq('userId', this.userId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error || !result) return null;
|
||||
return this.transform(result);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
const { error } = await this.supabase.from('pokedexes').delete().eq('id', id).eq('userId', this.userId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting pokedex:', error);
|
||||
throw new Error(`Failed to delete pokedex: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PokedexRepository;
|
||||
Reference in New Issue
Block a user