data(*): Add final data changes

This commit is contained in:
Josh Creek
2026-01-17 23:30:40 +00:00
parent 479b3ae7a0
commit 46351c9af0
29 changed files with 12187 additions and 122 deletions
+52
View File
@@ -0,0 +1,52 @@
dexId,gameDisplayName,dexDisplayName,sortOrder,isDlc,parentDexId,file
red-kanto,Red,Kanto,1,false,,native-game-dexes/red-blue-yellow.csv
blue-kanto,Blue,Kanto,1,false,,native-game-dexes/red-blue-yellow.csv
yellow-kanto,Yellow,Kanto,1,false,,native-game-dexes/red-blue-yellow.csv
gold-johto,Gold,Johto,1,false,,native-game-dexes/gold-silver-crystal.csv
silver-johto,Silver,Johto,1,false,,native-game-dexes/gold-silver-crystal.csv
crystal-johto,Crystal,Johto,1,false,,native-game-dexes/gold-silver-crystal.csv
ruby-hoenn,Ruby,Hoenn,1,false,,native-game-dexes/ruby-sapphire-emerald.csv
sapphire-hoenn,Sapphire,Hoenn,1,false,,native-game-dexes/ruby-sapphire-emerald.csv
emerald-hoenn,Emerald,Hoenn,1,false,,native-game-dexes/ruby-sapphire-emerald.csv
firered-kanto,FireRed,Kanto,1,false,,native-game-dexes/fire-red-leaf-green.csv
leafgreen-kanto,LeafGreen,Kanto,1,false,,native-game-dexes/fire-red-leaf-green.csv
diamond-sinnoh,Diamond,Sinnoh,1,false,,native-game-dexes/diamond-pearl.csv
pearl-sinnoh,Pearl,Sinnoh,1,false,,native-game-dexes/diamond-pearl.csv
platinum-sinnoh,Platinum,Sinnoh,1,false,,native-game-dexes/platinum.csv
heart-gold-johto,Heart Gold,Johto,1,false,,native-game-dexes/heart-gold-soul-silver.csv
soul-silver-johto,Soul Silver,Johto,1,false,,native-game-dexes/heart-gold-soul-silver.csv
black-unova,Black,Unova,1,false,,native-game-dexes/black-white.csv
white-unova,White,Unova,1,false,,native-game-dexes/black-white.csv
black-2-unova,Black 2,Unova,1,false,,native-game-dexes/black-2-white-2.csv
white-2-unova,White 2,Unova,1,false,,native-game-dexes/black-2-white-2.csv
x-central-kalos,X,Central Kalos,1,false,,native-game-dexes/x-y-central-kalos.csv
x-coastal-kalos,X,Coastal Kalos,2,false,,native-game-dexes/x-y-coastal-kalos.csv
x-mountain-kalos,X,Mountain Kalos,3,false,,native-game-dexes/x-y-mountain-kalos.csv
y-central-kalos,Y,Central Kalos,1,false,,native-game-dexes/x-y-central-kalos.csv
y-coastal-kalos,Y,Coastal Kalos,2,false,,native-game-dexes/x-y-coastal-kalos.csv
y-mountain-kalos,Y,Mountain Kalos,3,false,,native-game-dexes/x-y-mountain-kalos.csv
omega-ruby-hoenn,Omega Ruby,Hoenn,1,false,,native-game-dexes/omega-ruby-alpha-sapphire.csv
alpha-sapphire-hoenn,Alpha Sapphire,Hoenn,1,false,,native-game-dexes/omega-ruby-alpha-sapphire.csv
sun-alola,Sun,Alola,1,false,,native-game-dexes/sun-moon-alola.csv
moon-alola,Moon,Alola,1,false,,native-game-dexes/sun-moon-alola.csv
ultra-sun-alola,Ultra Sun,Alola,1,false,,native-game-dexes/ultra-sun-ultra-moon-alola.csv
ultra-moon-alola,Ultra Moon,Alola,1,false,,native-game-dexes/ultra-sun-ultra-moon-alola.csv
lg-pikachu-kanto,LG: Pikachu,Kanto,1,false,,native-game-dexes/lets-go-pikachu-eevee.csv
lg-eevee-kanto,LG: Eevee,Kanto,1,false,,native-game-dexes/lets-go-pikachu-eevee.csv
sword-galar,Sword,Galar,1,false,,native-game-dexes/sword-shield.csv
sword-isle-armor,Sword,Isle of Armor,2,true,sword-galar,native-game-dexes/sword-shield-isle-armor.csv
sword-crown-tundra,Sword,Crown Tundra,3,true,sword-galar,native-game-dexes/sword-shield-crown-tundra.csv
shield-galar,Shield,Galar,1,false,,native-game-dexes/sword-shield.csv
shield-isle-armor,Shield,Isle of Armor,2,true,shield-galar,native-game-dexes/sword-shield-isle-armor.csv
shield-crown-tundra,Shield,Crown Tundra,3,true,shield-galar,native-game-dexes/sword-shield-crown-tundra.csv
brilliant-diamond-sinnoh,Brilliant Diamond,Sinnoh,1,false,,native-game-dexes/brilliant-diamond-shining-pearl.csv
shining-pearl-sinnoh,Shining Pearl,Sinnoh,1,false,,native-game-dexes/brilliant-diamond-shining-pearl.csv
legends-arceus-hisui,Legends: Arceus,Hisui,1,false,,native-game-dexes/legends-arceus.csv
scarlet-paldea,Scarlet,Paldea,1,false,,native-game-dexes/scarlet-violet.csv
scarlet-kitakami,Scarlet,Kitakami,2,true,scarlet-paldea,native-game-dexes/scarlet-violet-kitakami.csv
scarlet-blueberry,Scarlet,Blueberry,3,true,scarlet-paldea,native-game-dexes/scarlet-violet-blueberry.csv
violet-paldea,Violet,Paldea,1,false,,native-game-dexes/scarlet-violet.csv
violet-kitakami,Violet,Kitakami,2,true,violet-paldea,native-game-dexes/scarlet-violet-kitakami.csv
violet-blueberry,Violet,Blueberry,3,true,violet-paldea,native-game-dexes/scarlet-violet-blueberry.csv
legends-z-a-kalos,Legends: Z-A,Kalos,1,false,,native-game-dexes/legends-z-a.csv
legends-z-a-mega-dimension,Legends: Z-A,Mega Dimension,2,true,legends-z-a-kalos,native-game-dexes/legends-z-a-mega-dimension.csv
1 dexId gameDisplayName dexDisplayName sortOrder isDlc parentDexId file
2 red-kanto Red Kanto 1 false native-game-dexes/red-blue-yellow.csv
3 blue-kanto Blue Kanto 1 false native-game-dexes/red-blue-yellow.csv
4 yellow-kanto Yellow Kanto 1 false native-game-dexes/red-blue-yellow.csv
5 gold-johto Gold Johto 1 false native-game-dexes/gold-silver-crystal.csv
6 silver-johto Silver Johto 1 false native-game-dexes/gold-silver-crystal.csv
7 crystal-johto Crystal Johto 1 false native-game-dexes/gold-silver-crystal.csv
8 ruby-hoenn Ruby Hoenn 1 false native-game-dexes/ruby-sapphire-emerald.csv
9 sapphire-hoenn Sapphire Hoenn 1 false native-game-dexes/ruby-sapphire-emerald.csv
10 emerald-hoenn Emerald Hoenn 1 false native-game-dexes/ruby-sapphire-emerald.csv
11 firered-kanto FireRed Kanto 1 false native-game-dexes/fire-red-leaf-green.csv
12 leafgreen-kanto LeafGreen Kanto 1 false native-game-dexes/fire-red-leaf-green.csv
13 diamond-sinnoh Diamond Sinnoh 1 false native-game-dexes/diamond-pearl.csv
14 pearl-sinnoh Pearl Sinnoh 1 false native-game-dexes/diamond-pearl.csv
15 platinum-sinnoh Platinum Sinnoh 1 false native-game-dexes/platinum.csv
16 heart-gold-johto Heart Gold Johto 1 false native-game-dexes/heart-gold-soul-silver.csv
17 soul-silver-johto Soul Silver Johto 1 false native-game-dexes/heart-gold-soul-silver.csv
18 black-unova Black Unova 1 false native-game-dexes/black-white.csv
19 white-unova White Unova 1 false native-game-dexes/black-white.csv
20 black-2-unova Black 2 Unova 1 false native-game-dexes/black-2-white-2.csv
21 white-2-unova White 2 Unova 1 false native-game-dexes/black-2-white-2.csv
22 x-central-kalos X Central Kalos 1 false native-game-dexes/x-y-central-kalos.csv
23 x-coastal-kalos X Coastal Kalos 2 false native-game-dexes/x-y-coastal-kalos.csv
24 x-mountain-kalos X Mountain Kalos 3 false native-game-dexes/x-y-mountain-kalos.csv
25 y-central-kalos Y Central Kalos 1 false native-game-dexes/x-y-central-kalos.csv
26 y-coastal-kalos Y Coastal Kalos 2 false native-game-dexes/x-y-coastal-kalos.csv
27 y-mountain-kalos Y Mountain Kalos 3 false native-game-dexes/x-y-mountain-kalos.csv
28 omega-ruby-hoenn Omega Ruby Hoenn 1 false native-game-dexes/omega-ruby-alpha-sapphire.csv
29 alpha-sapphire-hoenn Alpha Sapphire Hoenn 1 false native-game-dexes/omega-ruby-alpha-sapphire.csv
30 sun-alola Sun Alola 1 false native-game-dexes/sun-moon-alola.csv
31 moon-alola Moon Alola 1 false native-game-dexes/sun-moon-alola.csv
32 ultra-sun-alola Ultra Sun Alola 1 false native-game-dexes/ultra-sun-ultra-moon-alola.csv
33 ultra-moon-alola Ultra Moon Alola 1 false native-game-dexes/ultra-sun-ultra-moon-alola.csv
34 lg-pikachu-kanto LG: Pikachu Kanto 1 false native-game-dexes/lets-go-pikachu-eevee.csv
35 lg-eevee-kanto LG: Eevee Kanto 1 false native-game-dexes/lets-go-pikachu-eevee.csv
36 sword-galar Sword Galar 1 false native-game-dexes/sword-shield.csv
37 sword-isle-armor Sword Isle of Armor 2 true sword-galar native-game-dexes/sword-shield-isle-armor.csv
38 sword-crown-tundra Sword Crown Tundra 3 true sword-galar native-game-dexes/sword-shield-crown-tundra.csv
39 shield-galar Shield Galar 1 false native-game-dexes/sword-shield.csv
40 shield-isle-armor Shield Isle of Armor 2 true shield-galar native-game-dexes/sword-shield-isle-armor.csv
41 shield-crown-tundra Shield Crown Tundra 3 true shield-galar native-game-dexes/sword-shield-crown-tundra.csv
42 brilliant-diamond-sinnoh Brilliant Diamond Sinnoh 1 false native-game-dexes/brilliant-diamond-shining-pearl.csv
43 shining-pearl-sinnoh Shining Pearl Sinnoh 1 false native-game-dexes/brilliant-diamond-shining-pearl.csv
44 legends-arceus-hisui Legends: Arceus Hisui 1 false native-game-dexes/legends-arceus.csv
45 scarlet-paldea Scarlet Paldea 1 false native-game-dexes/scarlet-violet.csv
46 scarlet-kitakami Scarlet Kitakami 2 true scarlet-paldea native-game-dexes/scarlet-violet-kitakami.csv
47 scarlet-blueberry Scarlet Blueberry 3 true scarlet-paldea native-game-dexes/scarlet-violet-blueberry.csv
48 violet-paldea Violet Paldea 1 false native-game-dexes/scarlet-violet.csv
49 violet-kitakami Violet Kitakami 2 true violet-paldea native-game-dexes/scarlet-violet-kitakami.csv
50 violet-blueberry Violet Blueberry 3 true violet-paldea native-game-dexes/scarlet-violet-blueberry.csv
51 legends-z-a-kalos Legends: Z-A Kalos 1 false native-game-dexes/legends-z-a.csv
52 legends-z-a-mega-dimension Legends: Z-A Mega Dimension 2 true legends-z-a-kalos native-game-dexes/legends-z-a-mega-dimension.csv
+4 -2
View File
@@ -71,11 +71,12 @@
<!-- Email Input -->
<div class="form-control">
<label class="label">
<label class="label" for="signin-email">
<span class="label-text font-medium">Email</span>
</label>
<div class="relative">
<input
id="signin-email"
type="email"
placeholder="your@email.com"
class="input input-bordered w-full pl-10"
@@ -101,11 +102,12 @@
<!-- Password Input -->
<div class="form-control">
<label class="label">
<label class="label" for="signin-password">
<span class="label-text font-medium">Password</span>
</label>
<div class="relative">
<input
id="signin-password"
type="password"
placeholder="••••••••"
class="input input-bordered w-full pl-10"
+4 -2
View File
@@ -38,10 +38,11 @@
<form class="card-body">
<div class="form-control">
<label class="label">
<label class="label" for="signup-email">
<span class="label-text">Email</span>
</label>
<input
id="signup-email"
type="email"
placeholder="email"
class="input input-bordered"
@@ -50,10 +51,11 @@
/>
</div>
<div class="form-control">
<label class="label">
<label class="label" for="signup-password">
<span class="label-text">Password</span>
</label>
<input
id="signup-password"
type="password"
placeholder="password"
class="input input-bordered"
+5 -3
View File
@@ -7,14 +7,16 @@
</script>
<div class="relative inline-block">
<div
class="tooltip-trigger cursor-pointer"
<button
type="button"
class="tooltip-trigger cursor-pointer bg-transparent border-0 p-0 appearance-none"
aria-expanded={show}
on:click={handleClick}
on:mouseenter={() => (show = true)}
on:mouseleave={() => (show = false)}
>
<slot name="hover-target" />
</div>
</button>
{#if show}
<div class="tooltip-content absolute z-10 mt-2 w-52">
@@ -36,6 +36,12 @@
<span class="font-semibold">Game:</span>
{pokedex.gameScope}
</div>
{#if pokedex.dexScopes?.length}
<div class="text-sm text-base-content/70">
<span class="font-semibold">Dexes:</span>
{pokedex.dexScopes.length}
</div>
{/if}
{:else}
<div class="text-sm text-base-content/70">
<span class="font-semibold">Scope:</span> All Games
@@ -1,6 +1,6 @@
<script lang="ts">
import type { CatchRecord } from '$lib/models/CatchRecord';
import type { PokedexEntry } from '$lib/models/PokedexEntry';
import type { CatchInformationItem, PokedexEntry } from '$lib/models/PokedexEntry';
import PokemonSprite from '../PokemonSprite.svelte';
import { createEventDispatcher } from 'svelte';
@@ -31,6 +31,10 @@
type UpdateCatchSource = 'toggle' | 'notes' | 'notes-blur';
const isCatchInformationItem = (
value: string | CatchInformationItem
): value is CatchInformationItem => typeof value !== 'string';
function updateCatchRecord(source: UpdateCatchSource) {
dispatch('updateCatch', { pokedexEntry, catchRecord, source });
}
@@ -193,11 +197,15 @@
<ul class="list-disc list-inside">
{#each pokedexEntry.catchInformation as info}
<li>
<ul>
<li><strong>Game:</strong> {info.game}</li>
<li><strong>Location:</strong> {info.location}</li>
<li><strong>Notes:</strong> {info.notes}</li>
</ul>
{#if isCatchInformationItem(info)}
<ul>
<li><strong>Game:</strong> {info.game}</li>
<li><strong>Location:</strong> {info.location}</li>
<li><strong>Notes:</strong> {info.notes}</li>
</ul>
{:else}
{info}
{/if}
</li>
{/each}
</ul>
+122 -39
View File
@@ -9,36 +9,83 @@
isShinyDex: false,
isOriginDex: false,
isFormDex: false,
gameScope: null
gameScope: null,
dexScopes: []
};
export let mode: 'create' | 'edit' = 'create';
export let onSubmit: () => void;
export let onCancel: () => void;
let games: string[] = [];
let loadingGames = true;
type GameDex = {
id: string;
displayName: string;
sortOrder: number;
isDlc: boolean;
parentDexId: string | null;
};
type GameSummary = {
displayName: string;
releaseYear: number;
};
// Fetch available games from the database
let gameDexes: Record<string, GameDex[]> = {};
let gameOrder: string[] = [];
let gameList: GameSummary[] = [];
let availableDexes: GameDex[] = [];
let loadingDexes = true;
let lastGameScope: string | null = null;
let dexScopesInitialized = false;
let hasSeenGameScope = false;
// Fetch available game dexes from the database
onMount(async () => {
try {
const response = await fetch('/api/games');
const response = await fetch('/api/game-dexes');
if (response.ok) {
const data = await response.json();
games = data.games || [];
gameDexes = data.gameDexes || {};
gameOrder = data.gameOrder || Object.keys(gameDexes);
gameList = data.games || [];
} else {
console.error('Failed to fetch games');
console.error('Failed to fetch game dexes');
}
} catch (error) {
console.error('Error fetching games:', error);
console.error('Error fetching game dexes:', error);
} finally {
loadingGames = false;
loadingDexes = false;
}
});
// Validation
$: hasAtLeastOneType =
pokedex.isLivingDex || pokedex.isShinyDex || pokedex.isOriginDex || pokedex.isFormDex;
$: canSubmit = pokedex.name && pokedex.name.trim() !== '' && hasAtLeastOneType;
$: hasDexScope =
!pokedex.gameScope || (pokedex.dexScopes && pokedex.dexScopes.length > 0);
$: canSubmit =
pokedex.name && pokedex.name.trim() !== '' && hasAtLeastOneType && hasDexScope;
$: if (pokedex.gameScope !== lastGameScope) {
const shouldResetDexes = mode === 'create' || hasSeenGameScope;
lastGameScope = pokedex.gameScope || null;
dexScopesInitialized = false;
if (shouldResetDexes) {
pokedex.dexScopes = [];
}
if (!pokedex.gameScope) {
availableDexes = [];
}
hasSeenGameScope = true;
}
$: if (!loadingDexes && pokedex.gameScope) {
availableDexes = gameDexes[pokedex.gameScope] || [];
if (!dexScopesInitialized) {
if (!pokedex.dexScopes || pokedex.dexScopes.length === 0) {
pokedex.dexScopes = availableDexes.map((dex) => dex.id);
}
dexScopesInitialized = true;
}
}
function handleSubmit() {
if (canSubmit) {
@@ -74,30 +121,32 @@
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Type(s)</span>
<span class="label-text-alt text-error"
>{!hasAtLeastOneType ? 'At least one type required' : ''}</span
>
</label>
<div class="flex flex-col gap-2">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isLivingDex} />
<span class="label-text">Living Dex (one of each Pokémon)</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isShinyDex} />
<span class="label-text">Shiny Dex (shiny variants only)</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isOriginDex} />
<span class="label-text">Origin Dex (caught in native regions)</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isFormDex} />
<span class="label-text">Form Dex (all forms included)</span>
</label>
</div>
<fieldset class="w-full">
<legend class="label">
<span class="label-text">Type(s)</span>
<span class="label-text-alt text-error"
>{!hasAtLeastOneType ? 'At least one type required' : ''}</span
>
</legend>
<div class="flex flex-col gap-2">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isLivingDex} />
<span class="label-text">Living Dex (one of each Pokémon)</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isShinyDex} />
<span class="label-text">Shiny Dex (shiny variants only)</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isOriginDex} />
<span class="label-text">Origin Dex (caught in native regions)</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isFormDex} />
<span class="label-text">Form Dex (all forms included)</span>
</label>
</div>
</fieldset>
</div>
<div class="form-control w-full">
@@ -108,19 +157,53 @@
id="game-scope"
class="select select-bordered"
bind:value={pokedex.gameScope}
disabled={loadingGames}
disabled={loadingDexes}
>
<option value={null}>All Games</option>
{#if loadingGames}
{#if loadingDexes}
<option disabled>Loading games...</option>
{:else}
{#each games as game}
<option value={game}>{game}</option>
{/each}
{#if gameList.length > 0}
{#each gameList as game}
<option value={game.displayName}>{game.displayName}</option>
{/each}
{:else}
{#each gameOrder.length > 0 ? gameOrder : Object.keys(gameDexes) as game}
<option value={game}>{game}</option>
{/each}
{/if}
{/if}
</select>
</div>
{#if pokedex.gameScope}
<div class="form-control w-full">
<fieldset class="w-full">
<legend class="label">
<span class="label-text">Dex Scope</span>
<span class="label-text-alt text-error">{!hasDexScope ? 'Select at least one dex' : ''}</span>
</legend>
{#if availableDexes.length === 0}
<p class="text-sm text-error">No dexes found for this game.</p>
{:else}
<div class="flex flex-col gap-2">
{#each availableDexes as dex}
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
class="checkbox"
value={dex.id}
bind:group={pokedex.dexScopes}
/>
<span class="label-text">{dex.displayName}</span>
</label>
{/each}
</div>
{/if}
</fieldset>
</div>
{/if}
<div class="modal-action">
<button class="btn btn-ghost" type="button" on:click={onCancel}>Cancel</button>
<button class="btn btn-primary" type="button" on:click={handleSubmit} disabled={!canSubmit}>
@@ -31,11 +31,12 @@
<slot />
</div>
</div>
<div
<button
type="button"
class="modal-backdrop bg-black/50"
aria-label="Close modal"
on:click={handleBackdropClick}
on:keydown={() => {}}
></div>
></button>
</div>
{/if}
@@ -46,6 +47,8 @@
left: 0;
width: 100%;
height: 100%;
border: 0;
padding: 0;
}
.modal-box-custom {
@@ -353,7 +353,7 @@
<div class="mb-8">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4 relative z-20">
<h2 class="text-xl font-bold">Box {boxNumber}</h2>
<div class="relative" on:click|stopPropagation>
<div class="relative">
<button
type="button"
class="btn btn-sm btn-outline relative z-[210]"
@@ -376,12 +376,11 @@
<ul
id={bulkMenuId}
class="menu bg-base-100 rounded-box absolute right-0 mt-2 z-[220] w-56 p-2 shadow border border-base-300"
on:click|stopPropagation
>
<li>
<button
type="button"
on:click={() => {
on:click|stopPropagation={() => {
markBoxAsNotCaught(boxNumber);
openBulkMenuForBox = null;
}}
@@ -392,7 +391,7 @@
<li>
<button
type="button"
on:click={() => {
on:click|stopPropagation={() => {
markBoxAsCaught(boxNumber);
openBulkMenuForBox = null;
}}
@@ -403,7 +402,7 @@
<li>
<button
type="button"
on:click={() => {
on:click|stopPropagation={() => {
markBoxAsNeedsToEvolve(boxNumber);
openBulkMenuForBox = null;
}}
@@ -414,7 +413,7 @@
<li>
<button
type="button"
on:click={() => {
on:click|stopPropagation={() => {
markBoxAsInHome(boxNumber);
openBulkMenuForBox = null;
}}
@@ -425,7 +424,7 @@
<li>
<button
type="button"
on:click={() => {
on:click|stopPropagation={() => {
markBoxAsNotInHome(boxNumber);
openBulkMenuForBox = null;
}}
+1
View File
@@ -8,6 +8,7 @@ export interface Pokedex {
isOriginDex: boolean;
isFormDex: boolean;
gameScope: string | null;
dexScopes: string[];
}
export interface PokedexDB {
+7 -1
View File
@@ -1,4 +1,10 @@
// Supabase-based PokedexEntry interface
export type CatchInformationItem = {
game: string;
location: string;
notes: string;
};
export interface PokedexEntry {
_id: string; // Maps to Supabase 'id' field for frontend compatibility
pokedexNumber: number;
@@ -9,7 +15,7 @@ export interface PokedexEntry {
gamesToCatchIn: string[];
regionToEvolveIn: string;
evolutionInformation: string;
catchInformation: string[];
catchInformation: Array<string | CatchInformationItem>;
// Note: Regional dex numbers stored in separate regional_dex_numbers table
}
+109 -9
View File
@@ -47,8 +47,8 @@ class CombinedDataRepository {
let query = this.supabase.from('pokedex_entries').select('*');
if (!enableForms) {
// Filter to only base forms (form is NULL) OR Unown "A" (since Unown has no base form)
query = query.or('form.is.null,and(pokemon.eq.Unown,form.eq.A)');
// Filter to base forms: NULL or 'male' (gendered species), plus Unown "A".
query = query.or('form.is.null,form.eq.male,and(pokemon.eq.Unown,form.eq.A)');
}
if (region) {
@@ -71,6 +71,83 @@ class CombinedDataRepository {
return query;
}
private buildDexEntriesQuery(dexScopes: string[], enableForms: boolean, region: string) {
let query = this.supabase.from('game_pokedex_entry_details').select('*').in('dexId', dexScopes);
if (!enableForms) {
// Filter to base forms: NULL or 'male' (gendered species), plus Unown "A".
query = query.or('form.is.null,form.eq.male,and(pokemon.eq.Unown,form.eq.A)');
}
if (region) {
query = query.eq('regionToCatchIn', region);
}
// Stable ordering: dex order, dex number, Unown order, base/female/temporal, then form label.
query = query
.order('dexSortOrder', { ascending: true })
.order('dexNumber', { ascending: true })
.order('unownSortOrder', { ascending: true })
.order('formSortBucket', { ascending: true })
.order('formSortRegionOrder', { ascending: true })
.order('formSortRegionalSub', { ascending: true })
.order('formSortLabel', { ascending: true });
return query;
}
private async fetchAllDexEntries(
dexScopes: string[],
enableForms: boolean,
region: string
): Promise<PokedexEntryDB[]> {
if (dexScopes.length === 0) return [];
const entries: PokedexEntryDB[] = [];
let start = 0;
const maxRows = CombinedDataRepository.MAX_ROWS_PER_REQUEST;
while (true) {
const end = start + maxRows - 1;
const { data, error } = await this.buildDexEntriesQuery(dexScopes, enableForms, region).range(
start,
end
);
if (error) {
console.error('Error finding dex-scoped combined data:', error);
return [];
}
if (!data || data.length === 0) {
break;
}
entries.push(...data);
if (data.length < maxRows) {
break;
}
start = end + 1;
}
return entries;
}
private dedupeEntries(entries: PokedexEntryDB[]): PokedexEntryDB[] {
const seen = new Set<number>();
const deduped: PokedexEntryDB[] = [];
for (const entry of entries) {
if (seen.has(entry.id)) continue;
seen.add(entry.id);
deduped.push(entry);
}
return deduped;
}
private async fetchEntriesByRange(
from: number,
to: number,
@@ -180,9 +257,13 @@ class CombinedDataRepository {
userId: string,
enableForms: boolean = true,
region: string = '',
game: string = ''
game: string = '',
dexScopes: string[] = []
): Promise<CombinedData[]> {
const entries = await this.fetchAllEntries(enableForms, region, game);
const entries =
dexScopes.length > 0
? this.dedupeEntries(await this.fetchAllDexEntries(dexScopes, enableForms, region))
: await this.fetchAllEntries(enableForms, region, game);
if (!entries || entries.length === 0) {
return [];
@@ -220,11 +301,18 @@ class CombinedDataRepository {
limit: number = 20,
enableForms: boolean = true,
region: string = '',
game: string = ''
game: string = '',
dexScopes: string[] = []
): Promise<CombinedData[]> {
const from = (page - 1) * limit;
const to = from + limit - 1;
const entries = await this.fetchEntriesByRange(from, to, enableForms, region, game);
const entries =
dexScopes.length > 0
? this.dedupeEntries(await this.fetchAllDexEntries(dexScopes, enableForms, region)).slice(
from,
to + 1
)
: await this.fetchEntriesByRange(from, to, enableForms, region, game);
if (!entries || entries.length === 0) {
return [];
@@ -256,13 +344,25 @@ class CombinedDataRepository {
return combinedData;
}
async countCombinedData(enableForms: boolean, region: string, game: string): Promise<number> {
async countCombinedData(
enableForms: boolean,
region: string,
game: string,
dexScopes: string[] = []
): Promise<number> {
if (dexScopes.length > 0) {
const entries = this.dedupeEntries(
await this.fetchAllDexEntries(dexScopes, enableForms, region)
);
return entries.length;
}
let query = this.supabase.from('pokedex_entries').select('id', { count: 'exact', head: true });
// Apply same filters as in findCombinedData
if (!enableForms) {
// Filter to only base forms (form is NULL) OR Unown "A" (since Unown has no base form)
query = query.or('form.is.null,and(pokemon.eq.Unown,form.eq.A)');
// Filter to base forms: NULL or 'male' (gendered species), plus Unown "A".
query = query.or('form.is.null,form.eq.male,and(pokemon.eq.Unown,form.eq.A)');
}
if (region) {
+44 -4
View File
@@ -1,9 +1,45 @@
import { type PokedexEntry, type PokedexEntryDB } from '$lib/models/PokedexEntry';
import {
type CatchInformationItem,
type PokedexEntry,
type PokedexEntryDB
} from '$lib/models/PokedexEntry';
import type { SupabaseClient } from '@supabase/supabase-js';
class PokedexEntryRepository {
constructor(private supabase: SupabaseClient) {}
private parseCatchInformation(
values: string[] | null
): Array<string | CatchInformationItem> {
if (!values) return [];
return values.map((value) => {
const trimmed = value.trim();
if (!trimmed.startsWith('{')) return value;
try {
const parsed = JSON.parse(trimmed) as Partial<CatchInformationItem>;
if (
parsed &&
typeof parsed.game === 'string' &&
typeof parsed.location === 'string' &&
typeof parsed.notes === 'string'
) {
return {
game: parsed.game,
location: parsed.location,
notes: parsed.notes
};
}
} catch {
return value;
}
return value;
});
}
private serializeCatchInformation(values: Array<string | CatchInformationItem>): string[] {
return values.map((value) => (typeof value === 'string' ? value : JSON.stringify(value)));
}
// Transform Supabase data to match frontend expectations (minimal transformation)
private transformPokedexEntry(entry: PokedexEntryDB): PokedexEntry {
return {
@@ -16,7 +52,7 @@ class PokedexEntryRepository {
gamesToCatchIn: entry.gamesToCatchIn || [],
regionToEvolveIn: entry.regionToEvolveIn || '',
evolutionInformation: entry.evolutionInformation || '',
catchInformation: entry.catchInformation || []
catchInformation: this.parseCatchInformation(entry.catchInformation)
};
}
@@ -53,7 +89,9 @@ class PokedexEntryRepository {
if (data.regionToEvolveIn !== undefined) dbData.regionToEvolveIn = data.regionToEvolveIn;
if (data.evolutionInformation !== undefined)
dbData.evolutionInformation = data.evolutionInformation;
if (data.catchInformation !== undefined) dbData.catchInformation = data.catchInformation;
if (data.catchInformation !== undefined) {
dbData.catchInformation = this.serializeCatchInformation(data.catchInformation);
}
// Box placement is calculated dynamically - not stored in database
const { data: result, error } = await this.supabase
@@ -78,7 +116,9 @@ class PokedexEntryRepository {
if (data.regionToEvolveIn !== undefined) dbData.regionToEvolveIn = data.regionToEvolveIn;
if (data.evolutionInformation !== undefined)
dbData.evolutionInformation = data.evolutionInformation;
if (data.catchInformation !== undefined) dbData.catchInformation = data.catchInformation;
if (data.catchInformation !== undefined) {
dbData.catchInformation = this.serializeCatchInformation(data.catchInformation);
}
// Box placement is calculated dynamically - not stored in database
const { data: result, error } = await this.supabase
+24 -4
View File
@@ -7,7 +7,7 @@ class PokedexRepository {
private userId: string
) {}
private transform(db: PokedexDB): Pokedex {
private transform(db: PokedexDB, dexScopes: string[] = []): Pokedex {
return {
_id: db.id,
userId: db.userId,
@@ -17,7 +17,8 @@ class PokedexRepository {
isShinyDex: db.isShinyDex,
isOriginDex: db.isOriginDex,
isFormDex: db.isFormDex,
gameScope: db.gameScope
gameScope: db.gameScope,
dexScopes
};
}
@@ -33,6 +34,23 @@ class PokedexRepository {
return dbData;
}
private async fetchDexScopesMap(pokedexIds: string[]): Promise<Map<string, string[]>> {
if (pokedexIds.length === 0) return new Map();
const { data, error } = await this.supabase
.from('pokedex_dex_scopes')
.select('pokedexId, dexId')
.in('pokedexId', pokedexIds);
if (error || !data) return new Map();
const map = new Map<string, string[]>();
for (const row of data) {
if (!map.has(row.pokedexId)) map.set(row.pokedexId, []);
map.get(row.pokedexId)?.push(row.dexId);
}
return map;
}
async findById(id: string): Promise<Pokedex | null> {
const { data, error } = await this.supabase
.from('pokedexes')
@@ -42,7 +60,8 @@ class PokedexRepository {
.single();
if (error || !data) return null;
return this.transform(data);
const dexScopesMap = await this.fetchDexScopesMap([data.id]);
return this.transform(data, dexScopesMap.get(data.id) || []);
}
async findAll(): Promise<Pokedex[]> {
@@ -53,7 +72,8 @@ class PokedexRepository {
.order('createdAt', { ascending: true });
if (error || !data) return [];
return data.map((d) => this.transform(d));
const dexScopesMap = await this.fetchDexScopesMap(data.map((d) => d.id));
return data.map((d) => this.transform(d, dexScopesMap.get(d.id) || []));
}
async create(data: Partial<Pokedex>): Promise<Pokedex> {
@@ -1,24 +1,35 @@
import RegionGameMappingModel, { type RegionGameMapping } from '$lib/models/RegionGameMapping';
import type { RegionGameMapping } from '$lib/models/RegionGameMapping';
class RegionGameMappingRepository {
async findById(id: string): Promise<RegionGameMapping | null> {
return RegionGameMappingModel.findById(id).exec();
void id;
return null;
}
async findAll(): Promise<RegionGameMapping[]> {
return RegionGameMappingModel.find().exec();
return [];
}
async create(data: Partial<RegionGameMapping>): Promise<RegionGameMapping> {
return RegionGameMappingModel.create(data);
return {
id: data.id,
region: data.region ?? '',
game: data.game ?? ''
};
}
async update(id: string, data: Partial<RegionGameMapping>): Promise<RegionGameMapping | null> {
return RegionGameMappingModel.findByIdAndUpdate(id, data, { new: true }).exec();
void id;
if (!data.region || !data.game) return null;
return {
id: data.id,
region: data.region,
game: data.game
};
}
async delete(id: string): Promise<void> {
await RegionGameMappingModel.findByIdAndDelete(id).exec();
void id;
}
}
+135
View File
@@ -0,0 +1,135 @@
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Pokedex } from '$lib/models/Pokedex';
type GameDexRow = {
id: string;
displayName: string;
sortOrder: number;
isDlc: boolean;
parentDexId: string | null;
};
type GameSummary = {
displayName: string;
releaseYear: number;
};
type GameDexRowWithGame = GameDexRow & {
games: GameSummary | GameSummary[] | null;
};
async function getGameIdForScope(
supabase: SupabaseClient,
gameScope: string
): Promise<string | null> {
const { data, error } = await supabase
.from('games')
.select('id')
.eq('displayName', gameScope)
.maybeSingle();
if (error || !data) return null;
return data.id;
}
export async function resolveDexScopes(
supabase: SupabaseClient,
pokedex: Pokedex
): Promise<string[]> {
if (!pokedex.gameScope) return [];
if (pokedex.dexScopes && pokedex.dexScopes.length > 0) {
return pokedex.dexScopes;
}
const gameId = await getGameIdForScope(supabase, pokedex.gameScope);
if (!gameId) return [];
const { data, error } = await supabase
.from('game_dexes')
.select('id')
.eq('gameId', gameId)
.order('sortOrder', { ascending: true })
.order('displayName', { ascending: true });
if (error || !data) return [];
return data.map((row) => row.id);
}
export async function setPokedexDexScopes(
supabase: SupabaseClient,
pokedexId: string,
dexIds: string[]
): Promise<void> {
const uniqueDexIds = Array.from(new Set(dexIds));
const { error: deleteError } = await supabase
.from('pokedex_dex_scopes')
.delete()
.eq('pokedexId', pokedexId);
if (deleteError) {
throw new Error(`Failed to clear dex scopes: ${deleteError.message}`);
}
if (uniqueDexIds.length === 0) return;
const rows = uniqueDexIds.map((dexId) => ({
pokedexId,
dexId
}));
const { error: insertError } = await supabase.from('pokedex_dex_scopes').insert(rows);
if (insertError) {
throw new Error(`Failed to save dex scopes: ${insertError.message}`);
}
}
export async function listGameDexes(
supabase: SupabaseClient
): Promise<{
gameDexes: Record<string, GameDexRow[]>;
gameOrder: string[];
games: { displayName: string; releaseYear: number }[];
}> {
const { data, error } = await supabase
.from('game_dexes')
.select('id, displayName, sortOrder, isDlc, parentDexId, games!inner(displayName, releaseYear)')
.order('sortOrder', { ascending: true })
.order('displayName', { ascending: true });
if (error || !data) return { gameDexes: {}, gameOrder: [], games: [] };
const grouped: Record<string, GameDexRow[]> = {};
const releaseYears: Record<string, number> = {};
const rows = data as GameDexRowWithGame[];
for (const row of rows) {
const game = Array.isArray(row.games) ? row.games[0] : row.games;
if (!game) continue;
const gameName = game.displayName;
if (!grouped[gameName]) grouped[gameName] = [];
if (releaseYears[gameName] == null) {
releaseYears[gameName] = game.releaseYear;
}
grouped[gameName].push({
id: row.id,
displayName: row.displayName,
sortOrder: row.sortOrder,
isDlc: row.isDlc,
parentDexId: row.parentDexId
});
}
const games = Object.keys(grouped)
.map((name) => ({
displayName: name,
releaseYear: releaseYears[name] ?? 0
}))
.sort((a, b) => {
if (a.releaseYear !== b.releaseYear) return b.releaseYear - a.releaseYear;
return a.displayName.localeCompare(b.displayName);
});
const gameOrder = games.map((game) => game.displayName);
return { gameDexes: grouped, gameOrder, games };
}
+44 -1
View File
@@ -1,5 +1,6 @@
import type { Pokedex } from '$lib/models/Pokedex';
import type { SupabaseClient } from '@supabase/supabase-js';
import { resolveDexScopes } from '$lib/services/PokedexDexScopeService';
/**
* Calculate expected pokedex entries based on pokedex configuration
@@ -10,12 +11,54 @@ export async function calculateExpectedEntries(
supabase: SupabaseClient,
pokedex: Pokedex
): Promise<number[]> {
const dexScopes = await resolveDexScopes(supabase, pokedex);
if (dexScopes.length > 0) {
const { data: dexEntries, error: dexError } = await supabase
.from('game_pokedex_entries')
.select('pokemonId')
.in('dexId', dexScopes);
if (dexError) {
console.error('Error calculating dex-scoped entries:', dexError);
throw new Error(`Failed to calculate dex-scoped entries: ${dexError.message}`);
}
const uniqueIds = Array.from(new Set(dexEntries?.map((row) => row.pokemonId) || []));
if (uniqueIds.length === 0) return [];
if (!pokedex.isFormDex) {
const { data: entries, error: entryError } = await supabase
.from('pokedex_entries')
.select('id, pokemon, form')
.in('id', uniqueIds);
if (entryError) {
console.error('Error filtering base forms for dex scopes:', entryError);
throw new Error(`Failed to filter base forms: ${entryError.message}`);
}
return (
entries
?.filter(
(entry) =>
entry.form === null ||
entry.form === 'male' ||
(entry.pokemon === 'Unown' && entry.form === 'A')
)
.map((entry) => entry.id) || []
);
}
return uniqueIds;
}
let query = supabase.from('pokedex_entries').select('id');
// Apply form filter: if isFormDex is false, only include base forms
// Base forms have form IS NULL, except Unown which has no base form (use 'A')
if (!pokedex.isFormDex) {
query = query.or('form.is.null,and(pokemon.eq.Unown,form.eq.A)');
query = query.or('form.is.null,form.eq.male,and(pokemon.eq.Unown,form.eq.A)');
}
// Apply region filter: if gameScope is specified, filter by region
+26 -20
View File
@@ -11,6 +11,7 @@
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '';
const pwaDescription = (pwaAssetsHead as { description?: { content: string } }).description;
export let data;
let { supabase } = data;
@@ -22,8 +23,10 @@
});
onDestroy(unsubscribe);
onMount(async () => {
await getUser();
let authSubscription: { unsubscribe: () => void } | null = null;
onMount(() => {
void getUser();
// Listen for auth state changes to keep the user store in sync
const {
@@ -36,27 +39,30 @@
}
user.set(localUser);
});
authSubscription = subscription;
if (pwaInfo) {
const { registerSW } = await import('virtual:pwa-register');
registerSW({
immediate: true,
onRegistered(r) {
// uncomment following code if you want check for updates
// r && setInterval(() => {
// console.log('Checking for sw update')
// r.update()
// }, 20000 /* 20s for testing purposes */)
console.log(`SW Registered: ${r}`);
},
onRegisterError(error) {
console.log('SW registration error', error);
}
});
void (async () => {
const { registerSW } = await import('virtual:pwa-register');
registerSW({
immediate: true,
onRegistered(r) {
// uncomment following code if you want check for updates
// r && setInterval(() => {
// console.log('Checking for sw update')
// r.update()
// }, 20000 /* 20s for testing purposes */)
console.log(`SW Registered: ${r}`);
},
onRegisterError(error) {
console.log('SW registration error', error);
}
});
})();
}
return () => {
subscription.unsubscribe();
authSubscription?.unsubscribe();
};
});
@@ -80,8 +86,8 @@
{#if pwaAssetsHead.themeColor}
<meta name="theme-color" content={pwaAssetsHead.themeColor.content} />
{/if}
{#if pwaAssetsHead.description}
<meta name="description" content={pwaAssetsHead.description.content} />
{#if pwaDescription}
<meta name="description" content={pwaDescription.content} />
{/if}
{#each pwaAssetsHead.links as link}
<link {...link} />
+17
View File
@@ -0,0 +1,17 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { listGameDexes } from '$lib/services/PokedexDexScopeService';
/**
* GET /api/game-dexes
* Returns game dexes grouped by game display name.
*/
export const GET: RequestHandler = async ({ locals }) => {
try {
const { gameDexes, gameOrder, games } = await listGameDexes(locals.supabase);
return json({ gameDexes, gameOrder, games });
} catch (err) {
console.error('Unexpected error fetching game dexes:', err);
return json({ error: 'Internal server error' }, { status: 500 });
}
};
+9
View File
@@ -4,6 +4,7 @@ import PokedexRepository from '$lib/repositories/PokedexRepository';
import { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
import { populatePokedexMappings } from '$lib/services/PokedexMappingService';
import { resolveDexScopes, setPokedexDexScopes } from '$lib/services/PokedexDexScopeService';
// GET: List all pokedexes for user
export const GET = async (event: RequestEvent) => {
@@ -31,12 +32,20 @@ export const POST = async (event: RequestEvent) => {
const data: Partial<Pokedex> = await event.request.json();
requestedName = typeof data.name === 'string' ? data.name : undefined;
data.userId = userId;
const requestedDexScopes = Array.isArray(data.dexScopes) ? data.dexScopes : [];
const repo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await repo.create(data);
createdPokedexId = pokedex._id;
createdPokedexName = pokedex.name;
const resolvedDexScopes = await resolveDexScopes(event.locals.supabase, {
...pokedex,
dexScopes: requestedDexScopes
});
await setPokedexDexScopes(event.locals.supabase, pokedex._id, resolvedDexScopes);
pokedex.dexScopes = resolvedDexScopes;
// Populate pokedex_entries_mapping table with expected entries
try {
await populatePokedexMappings(event.locals.supabase, pokedex._id, pokedex);
+26 -2
View File
@@ -4,6 +4,7 @@ import PokedexRepository from '$lib/repositories/PokedexRepository';
import { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
import { recalculatePokedexMappings } from '$lib/services/PokedexMappingService';
import { resolveDexScopes, setPokedexDexScopes } from '$lib/services/PokedexDexScopeService';
// GET: Get single pokedex by ID
export const GET = async (event: RequestEvent) => {
@@ -71,12 +72,35 @@ export const PUT = async (event: RequestEvent) => {
return json({ error: 'Pokedex not found' }, { status: 404 });
}
const dexScopesProvided = Object.prototype.hasOwnProperty.call(data, 'dexScopes');
const requestedDexScopes =
dexScopesProvided && Array.isArray(data.dexScopes)
? data.dexScopes
: existingPokedex.dexScopes || [];
const resolvedDexScopes = await resolveDexScopes(event.locals.supabase, {
...pokedex,
dexScopes: requestedDexScopes
});
const existingDexScopes = new Set(existingPokedex.dexScopes || []);
const nextDexScopes = new Set(resolvedDexScopes);
const dexScopesChanged =
existingDexScopes.size !== nextDexScopes.size ||
[...existingDexScopes].some((dexId) => !nextDexScopes.has(dexId));
if (dexScopesChanged) {
await setPokedexDexScopes(event.locals.supabase, id, resolvedDexScopes);
pokedex.dexScopes = resolvedDexScopes;
}
// Recalculate pokedex_entries_mapping if configuration changed
// Only recalculate if isFormDex or gameScope actually changed (these are the only fields that affect mapping)
// Only recalculate if isFormDex, gameScope, or dexScopes changed
// Compare the persisted result (pokedex) to existingPokedex to catch changes that repo.update may normalize or default
const configChanged =
pokedex.isFormDex !== existingPokedex.isFormDex ||
pokedex.gameScope !== existingPokedex.gameScope;
pokedex.gameScope !== existingPokedex.gameScope ||
dexScopesChanged;
if (configChanged) {
await recalculatePokedexMappings(event.locals.supabase, id, pokedex);
@@ -3,6 +3,7 @@ import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import PokedexRepository from '$lib/repositories/PokedexRepository';
import { getOptionalUserId } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit';
import { resolveDexScopes } from '$lib/services/PokedexDexScopeService';
// GET: Get combined data (pokédex entries + catch records) for specific pokédex
export const GET = async (event: RequestEvent) => {
@@ -39,6 +40,7 @@ export const GET = async (event: RequestEvent) => {
// Use pokédex's gameScope as default filter if no manual game filter is set
const effectiveGame = game || pokedex.gameScope || '';
const dexScopes = await resolveDexScopes(event.locals.supabase, pokedex);
const repo = new CombinedDataRepository(event.locals.supabase, userId, pokedexId);
@@ -49,11 +51,12 @@ export const GET = async (event: RequestEvent) => {
limit,
enableForms,
region,
effectiveGame
effectiveGame,
dexScopes
);
// Get total count for pagination
const totalCount = await repo.countCombinedData(enableForms, region, effectiveGame);
const totalCount = await repo.countCombinedData(enableForms, region, effectiveGame, dexScopes);
const totalPages = Math.ceil(totalCount / limit);
return json({
+2 -1
View File
@@ -146,11 +146,12 @@
<!-- Email Input -->
<div class="form-control">
<label class="label">
<label class="label" for="forgot-password-email">
<span class="label-text font-medium">Email</span>
</label>
<div class="relative">
<input
id="forgot-password-email"
type="email"
placeholder="your@email.com"
class="input input-bordered w-full pl-10"
+10 -3
View File
@@ -15,7 +15,8 @@
isShinyDex: false,
isOriginDex: false,
isFormDex: false,
gameScope: null
gameScope: null,
dexScopes: []
};
$: mode = editingPokedex ? ('edit' as const) : ('create' as const);
@@ -55,7 +56,8 @@
isShinyDex: false,
isOriginDex: false,
isFormDex: false,
gameScope: null
gameScope: null,
dexScopes: []
};
showModal = true;
}
@@ -199,6 +201,11 @@
</h3>
<PokedexForm bind:pokedex={formData} {mode} onSubmit={handleSubmit} onCancel={closeModal} />
</div>
<div class="modal-backdrop" on:click={closeModal}></div>
<button
type="button"
class="modal-backdrop"
aria-label="Close modal"
on:click={closeModal}
></button>
</div>
{/if}
+5
View File
@@ -467,6 +467,11 @@
/>
</svg>
<span class="font-semibold">{pokedex.gameScope}</span>
{#if pokedex.dexScopes?.length}
<span class="text-xs text-base-content/60">
({pokedex.dexScopes.length} dex{pokedex.dexScopes.length === 1 ? '' : 'es'})
</span>
{/if}
</div>
{:else}
<div class="divider divider-horizontal mx-0"></div>
+4 -2
View File
@@ -174,11 +174,12 @@
<!-- New Password Input -->
<div class="form-control">
<label class="label">
<label class="label" for="reset-password-new">
<span class="label-text font-medium">New Password</span>
</label>
<div class="relative">
<input
id="reset-password-new"
type="password"
placeholder="••••••••"
class="input input-bordered w-full pl-10"
@@ -203,11 +204,12 @@
<!-- Confirm Password Input -->
<div class="form-control">
<label class="label">
<label class="label" for="reset-password-confirm">
<span class="label-text font-medium">Confirm Password</span>
</label>
<div class="relative">
<input
id="reset-password-confirm"
type="password"
placeholder="••••••••"
class="input input-bordered w-full pl-10"
@@ -35,6 +35,22 @@ CREATE TABLE games (
CREATE INDEX idx_games_region_id ON games("regionId");
CREATE INDEX idx_games_release_year ON games("releaseYear");
-- Reference data: game dexes (base, DLCs, islands, etc.)
CREATE TABLE game_dexes (
id TEXT PRIMARY KEY,
"gameId" TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
"displayName" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL,
"isDlc" BOOLEAN DEFAULT FALSE,
"parentDexId" TEXT REFERENCES game_dexes(id) ON DELETE SET NULL,
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ DEFAULT NOW(),
UNIQUE ("gameId", "displayName")
);
CREATE INDEX idx_game_dexes_game_id ON game_dexes("gameId");
CREATE INDEX idx_game_dexes_sort_order ON game_dexes("sortOrder");
-- Reference data: pokemon (one row per pokedexNumber + form)
CREATE TABLE pokemon (
id BIGSERIAL PRIMARY KEY,
@@ -74,14 +90,14 @@ CREATE INDEX idx_pokemon_origin_games_game_id ON pokemon_origin_games("gameId");
-- Reference data: per-game pokedex entries (seeded from per-game CSVs)
CREATE TABLE game_pokedex_entries (
"gameId" TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
"dexId" TEXT NOT NULL REFERENCES game_dexes(id) ON DELETE CASCADE,
"pokemonId" BIGINT NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
"dexNumber" INTEGER,
notes TEXT,
PRIMARY KEY ("gameId", "pokemonId")
PRIMARY KEY ("dexId", "pokemonId")
);
CREATE INDEX idx_game_pokedex_entries_game_id ON game_pokedex_entries("gameId");
CREATE INDEX idx_game_pokedex_entries_dex_id ON game_pokedex_entries("dexId");
CREATE INDEX idx_game_pokedex_entries_pokemon_id ON game_pokedex_entries("pokemonId");
-- User-owned pokedexes
@@ -108,6 +124,16 @@ CREATE TABLE pokedexes (
CREATE INDEX idx_pokedexes_user_id ON pokedexes("userId");
-- Selected dex scopes per user pokedex
CREATE TABLE pokedex_dex_scopes (
"pokedexId" UUID NOT NULL REFERENCES pokedexes(id) ON DELETE CASCADE,
"dexId" TEXT NOT NULL REFERENCES game_dexes(id) ON DELETE CASCADE,
PRIMARY KEY ("pokedexId", "dexId")
);
CREATE INDEX idx_pokedex_dex_scopes_pokedex_id ON pokedex_dex_scopes("pokedexId");
CREATE INDEX idx_pokedex_dex_scopes_dex_id ON pokedex_dex_scopes("dexId");
-- User-owned expected entries per pokedex
CREATE TABLE pokedex_pokemon_mapping (
id BIGSERIAL PRIMARY KEY,
@@ -167,6 +193,10 @@ CREATE TRIGGER update_games_updated_at
BEFORE UPDATE ON games
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_game_dexes_updated_at
BEFORE UPDATE ON game_dexes
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_pokemon_updated_at
BEFORE UPDATE ON pokemon
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
@@ -188,6 +218,10 @@ ALTER TABLE games ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Games are publicly readable" ON games
FOR SELECT TO public USING (true);
ALTER TABLE game_dexes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Game dexes are publicly readable" ON game_dexes
FOR SELECT TO public USING (true);
ALTER TABLE pokemon ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Pokemon are publicly readable" ON pokemon
FOR SELECT TO public USING (true);
@@ -210,6 +244,24 @@ CREATE POLICY "Users can update own pokedexes" ON pokedexes
CREATE POLICY "Users can delete own pokedexes" ON pokedexes
FOR DELETE USING (auth.uid() = "userId");
ALTER TABLE pokedex_dex_scopes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own dex scopes" ON pokedex_dex_scopes
FOR SELECT USING (
EXISTS (
SELECT 1 FROM pokedexes
WHERE pokedexes.id = pokedex_dex_scopes."pokedexId"
AND pokedexes."userId" = auth.uid()
)
);
CREATE POLICY "Users can manage own dex scopes" ON pokedex_dex_scopes
FOR ALL USING (
EXISTS (
SELECT 1 FROM pokedexes
WHERE pokedexes.id = pokedex_dex_scopes."pokedexId"
AND pokedexes."userId" = auth.uid()
)
);
ALTER TABLE pokedex_pokemon_mapping ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view pokedex pokemon mapping"
ON pokedex_pokemon_mapping
@@ -335,6 +387,20 @@ GROUP BY p.id, r.name, r."releaseOrder";
GRANT SELECT ON pokedex_entries TO anon, authenticated;
-- Dex-scoped view for ordering and filtering by native dexes
CREATE OR REPLACE VIEW game_pokedex_entry_details AS
SELECT
gpe."dexId",
gd."displayName" AS "dexDisplayName",
gd."sortOrder" AS "dexSortOrder",
gpe."dexNumber",
pe.*
FROM game_pokedex_entries gpe
JOIN game_dexes gd ON gd.id = gpe."dexId"
JOIN pokedex_entries pe ON pe.id = gpe."pokemonId";
GRANT SELECT ON game_pokedex_entry_details TO anon, authenticated;
-- RPC: recalculate expected mappings for a pokedex
CREATE OR REPLACE FUNCTION recalculate_pokedex_mappings(
p_pokedex_id UUID,
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -17,8 +17,15 @@
"virtual:pwa-assets"
]
},
"include": ["src/**/*.ts", "src/**/*.svelte", "node_modules/vite-plugin-pwa/client.d.ts"],
"exclude": [".svelte-kit/**/*", "node_modules/**/*"]
"include": [
".svelte-kit/ambient.d.ts",
".svelte-kit/non-ambient.d.ts",
".svelte-kit/types/**/$types.d.ts",
"src/**/*.ts",
"src/**/*.svelte",
"node_modules/vite-plugin-pwa/client.d.ts"
],
"exclude": ["node_modules/**/*"]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes