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 --> <!-- Email Input -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="signin-email">
<span class="label-text font-medium">Email</span> <span class="label-text font-medium">Email</span>
</label> </label>
<div class="relative"> <div class="relative">
<input <input
id="signin-email"
type="email" type="email"
placeholder="your@email.com" placeholder="your@email.com"
class="input input-bordered w-full pl-10" class="input input-bordered w-full pl-10"
@@ -101,11 +102,12 @@
<!-- Password Input --> <!-- Password Input -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="signin-password">
<span class="label-text font-medium">Password</span> <span class="label-text font-medium">Password</span>
</label> </label>
<div class="relative"> <div class="relative">
<input <input
id="signin-password"
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
class="input input-bordered w-full pl-10" class="input input-bordered w-full pl-10"
+4 -2
View File
@@ -38,10 +38,11 @@
<form class="card-body"> <form class="card-body">
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="signup-email">
<span class="label-text">Email</span> <span class="label-text">Email</span>
</label> </label>
<input <input
id="signup-email"
type="email" type="email"
placeholder="email" placeholder="email"
class="input input-bordered" class="input input-bordered"
@@ -50,10 +51,11 @@
/> />
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="signup-password">
<span class="label-text">Password</span> <span class="label-text">Password</span>
</label> </label>
<input <input
id="signup-password"
type="password" type="password"
placeholder="password" placeholder="password"
class="input input-bordered" class="input input-bordered"
+5 -3
View File
@@ -7,14 +7,16 @@
</script> </script>
<div class="relative inline-block"> <div class="relative inline-block">
<div <button
class="tooltip-trigger cursor-pointer" type="button"
class="tooltip-trigger cursor-pointer bg-transparent border-0 p-0 appearance-none"
aria-expanded={show}
on:click={handleClick} on:click={handleClick}
on:mouseenter={() => (show = true)} on:mouseenter={() => (show = true)}
on:mouseleave={() => (show = false)} on:mouseleave={() => (show = false)}
> >
<slot name="hover-target" /> <slot name="hover-target" />
</div> </button>
{#if show} {#if show}
<div class="tooltip-content absolute z-10 mt-2 w-52"> <div class="tooltip-content absolute z-10 mt-2 w-52">
@@ -36,6 +36,12 @@
<span class="font-semibold">Game:</span> <span class="font-semibold">Game:</span>
{pokedex.gameScope} {pokedex.gameScope}
</div> </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} {:else}
<div class="text-sm text-base-content/70"> <div class="text-sm text-base-content/70">
<span class="font-semibold">Scope:</span> All Games <span class="font-semibold">Scope:</span> All Games
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { CatchRecord } from '$lib/models/CatchRecord'; 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 PokemonSprite from '../PokemonSprite.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@@ -31,6 +31,10 @@
type UpdateCatchSource = 'toggle' | 'notes' | 'notes-blur'; type UpdateCatchSource = 'toggle' | 'notes' | 'notes-blur';
const isCatchInformationItem = (
value: string | CatchInformationItem
): value is CatchInformationItem => typeof value !== 'string';
function updateCatchRecord(source: UpdateCatchSource) { function updateCatchRecord(source: UpdateCatchSource) {
dispatch('updateCatch', { pokedexEntry, catchRecord, source }); dispatch('updateCatch', { pokedexEntry, catchRecord, source });
} }
@@ -193,11 +197,15 @@
<ul class="list-disc list-inside"> <ul class="list-disc list-inside">
{#each pokedexEntry.catchInformation as info} {#each pokedexEntry.catchInformation as info}
<li> <li>
<ul> {#if isCatchInformationItem(info)}
<li><strong>Game:</strong> {info.game}</li> <ul>
<li><strong>Location:</strong> {info.location}</li> <li><strong>Game:</strong> {info.game}</li>
<li><strong>Notes:</strong> {info.notes}</li> <li><strong>Location:</strong> {info.location}</li>
</ul> <li><strong>Notes:</strong> {info.notes}</li>
</ul>
{:else}
{info}
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
+122 -39
View File
@@ -9,36 +9,83 @@
isShinyDex: false, isShinyDex: false,
isOriginDex: false, isOriginDex: false,
isFormDex: false, isFormDex: false,
gameScope: null gameScope: null,
dexScopes: []
}; };
export let mode: 'create' | 'edit' = 'create'; export let mode: 'create' | 'edit' = 'create';
export let onSubmit: () => void; export let onSubmit: () => void;
export let onCancel: () => void; export let onCancel: () => void;
let games: string[] = []; type GameDex = {
let loadingGames = true; 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 () => { onMount(async () => {
try { try {
const response = await fetch('/api/games'); const response = await fetch('/api/game-dexes');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
games = data.games || []; gameDexes = data.gameDexes || {};
gameOrder = data.gameOrder || Object.keys(gameDexes);
gameList = data.games || [];
} else { } else {
console.error('Failed to fetch games'); console.error('Failed to fetch game dexes');
} }
} catch (error) { } catch (error) {
console.error('Error fetching games:', error); console.error('Error fetching game dexes:', error);
} finally { } finally {
loadingGames = false; loadingDexes = false;
} }
}); });
// Validation // Validation
$: hasAtLeastOneType = $: hasAtLeastOneType =
pokedex.isLivingDex || pokedex.isShinyDex || pokedex.isOriginDex || pokedex.isFormDex; 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() { function handleSubmit() {
if (canSubmit) { if (canSubmit) {
@@ -74,30 +121,32 @@
</div> </div>
<div class="form-control w-full"> <div class="form-control w-full">
<label class="label"> <fieldset class="w-full">
<span class="label-text">Type(s)</span> <legend class="label">
<span class="label-text-alt text-error" <span class="label-text">Type(s)</span>
>{!hasAtLeastOneType ? 'At least one type required' : ''}</span <span class="label-text-alt text-error"
> >{!hasAtLeastOneType ? 'At least one type required' : ''}</span
</label> >
<div class="flex flex-col gap-2"> </legend>
<label class="label cursor-pointer justify-start gap-3"> <div class="flex flex-col gap-2">
<input type="checkbox" class="checkbox" bind:checked={pokedex.isLivingDex} /> <label class="label cursor-pointer justify-start gap-3">
<span class="label-text">Living Dex (one of each Pokémon)</span> <input type="checkbox" class="checkbox" bind:checked={pokedex.isLivingDex} />
</label> <span class="label-text">Living Dex (one of each Pokémon)</span>
<label class="label cursor-pointer justify-start gap-3"> </label>
<input type="checkbox" class="checkbox" bind:checked={pokedex.isShinyDex} /> <label class="label cursor-pointer justify-start gap-3">
<span class="label-text">Shiny Dex (shiny variants only)</span> <input type="checkbox" class="checkbox" bind:checked={pokedex.isShinyDex} />
</label> <span class="label-text">Shiny Dex (shiny variants only)</span>
<label class="label cursor-pointer justify-start gap-3"> </label>
<input type="checkbox" class="checkbox" bind:checked={pokedex.isOriginDex} /> <label class="label cursor-pointer justify-start gap-3">
<span class="label-text">Origin Dex (caught in native regions)</span> <input type="checkbox" class="checkbox" bind:checked={pokedex.isOriginDex} />
</label> <span class="label-text">Origin Dex (caught in native regions)</span>
<label class="label cursor-pointer justify-start gap-3"> </label>
<input type="checkbox" class="checkbox" bind:checked={pokedex.isFormDex} /> <label class="label cursor-pointer justify-start gap-3">
<span class="label-text">Form Dex (all forms included)</span> <input type="checkbox" class="checkbox" bind:checked={pokedex.isFormDex} />
</label> <span class="label-text">Form Dex (all forms included)</span>
</div> </label>
</div>
</fieldset>
</div> </div>
<div class="form-control w-full"> <div class="form-control w-full">
@@ -108,19 +157,53 @@
id="game-scope" id="game-scope"
class="select select-bordered" class="select select-bordered"
bind:value={pokedex.gameScope} bind:value={pokedex.gameScope}
disabled={loadingGames} disabled={loadingDexes}
> >
<option value={null}>All Games</option> <option value={null}>All Games</option>
{#if loadingGames} {#if loadingDexes}
<option disabled>Loading games...</option> <option disabled>Loading games...</option>
{:else} {:else}
{#each games as game} {#if gameList.length > 0}
<option value={game}>{game}</option> {#each gameList as game}
{/each} <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} {/if}
</select> </select>
</div> </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"> <div class="modal-action">
<button class="btn btn-ghost" type="button" on:click={onCancel}>Cancel</button> <button class="btn btn-ghost" type="button" on:click={onCancel}>Cancel</button>
<button class="btn btn-primary" type="button" on:click={handleSubmit} disabled={!canSubmit}> <button class="btn btn-primary" type="button" on:click={handleSubmit} disabled={!canSubmit}>
@@ -31,11 +31,12 @@
<slot /> <slot />
</div> </div>
</div> </div>
<div <button
type="button"
class="modal-backdrop bg-black/50" class="modal-backdrop bg-black/50"
aria-label="Close modal"
on:click={handleBackdropClick} on:click={handleBackdropClick}
on:keydown={() => {}} ></button>
></div>
</div> </div>
{/if} {/if}
@@ -46,6 +47,8 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 0;
padding: 0;
} }
.modal-box-custom { .modal-box-custom {
@@ -353,7 +353,7 @@
<div class="mb-8"> <div class="mb-8">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4 relative z-20"> <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> <h2 class="text-xl font-bold">Box {boxNumber}</h2>
<div class="relative" on:click|stopPropagation> <div class="relative">
<button <button
type="button" type="button"
class="btn btn-sm btn-outline relative z-[210]" class="btn btn-sm btn-outline relative z-[210]"
@@ -376,12 +376,11 @@
<ul <ul
id={bulkMenuId} 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" 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> <li>
<button <button
type="button" type="button"
on:click={() => { on:click|stopPropagation={() => {
markBoxAsNotCaught(boxNumber); markBoxAsNotCaught(boxNumber);
openBulkMenuForBox = null; openBulkMenuForBox = null;
}} }}
@@ -392,7 +391,7 @@
<li> <li>
<button <button
type="button" type="button"
on:click={() => { on:click|stopPropagation={() => {
markBoxAsCaught(boxNumber); markBoxAsCaught(boxNumber);
openBulkMenuForBox = null; openBulkMenuForBox = null;
}} }}
@@ -403,7 +402,7 @@
<li> <li>
<button <button
type="button" type="button"
on:click={() => { on:click|stopPropagation={() => {
markBoxAsNeedsToEvolve(boxNumber); markBoxAsNeedsToEvolve(boxNumber);
openBulkMenuForBox = null; openBulkMenuForBox = null;
}} }}
@@ -414,7 +413,7 @@
<li> <li>
<button <button
type="button" type="button"
on:click={() => { on:click|stopPropagation={() => {
markBoxAsInHome(boxNumber); markBoxAsInHome(boxNumber);
openBulkMenuForBox = null; openBulkMenuForBox = null;
}} }}
@@ -425,7 +424,7 @@
<li> <li>
<button <button
type="button" type="button"
on:click={() => { on:click|stopPropagation={() => {
markBoxAsNotInHome(boxNumber); markBoxAsNotInHome(boxNumber);
openBulkMenuForBox = null; openBulkMenuForBox = null;
}} }}
+1
View File
@@ -8,6 +8,7 @@ export interface Pokedex {
isOriginDex: boolean; isOriginDex: boolean;
isFormDex: boolean; isFormDex: boolean;
gameScope: string | null; gameScope: string | null;
dexScopes: string[];
} }
export interface PokedexDB { export interface PokedexDB {
+7 -1
View File
@@ -1,4 +1,10 @@
// Supabase-based PokedexEntry interface // Supabase-based PokedexEntry interface
export type CatchInformationItem = {
game: string;
location: string;
notes: string;
};
export interface PokedexEntry { export interface PokedexEntry {
_id: string; // Maps to Supabase 'id' field for frontend compatibility _id: string; // Maps to Supabase 'id' field for frontend compatibility
pokedexNumber: number; pokedexNumber: number;
@@ -9,7 +15,7 @@ export interface PokedexEntry {
gamesToCatchIn: string[]; gamesToCatchIn: string[];
regionToEvolveIn: string; regionToEvolveIn: string;
evolutionInformation: string; evolutionInformation: string;
catchInformation: string[]; catchInformation: Array<string | CatchInformationItem>;
// Note: Regional dex numbers stored in separate regional_dex_numbers table // 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('*'); let query = this.supabase.from('pokedex_entries').select('*');
if (!enableForms) { if (!enableForms) {
// Filter to only base forms (form is NULL) OR Unown "A" (since Unown has no base form) // Filter to base forms: NULL or 'male' (gendered species), plus Unown "A".
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)');
} }
if (region) { if (region) {
@@ -71,6 +71,83 @@ class CombinedDataRepository {
return query; 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( private async fetchEntriesByRange(
from: number, from: number,
to: number, to: number,
@@ -180,9 +257,13 @@ class CombinedDataRepository {
userId: string, userId: string,
enableForms: boolean = true, enableForms: boolean = true,
region: string = '', region: string = '',
game: string = '' game: string = '',
dexScopes: string[] = []
): Promise<CombinedData[]> { ): 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) { if (!entries || entries.length === 0) {
return []; return [];
@@ -220,11 +301,18 @@ class CombinedDataRepository {
limit: number = 20, limit: number = 20,
enableForms: boolean = true, enableForms: boolean = true,
region: string = '', region: string = '',
game: string = '' game: string = '',
dexScopes: string[] = []
): Promise<CombinedData[]> { ): Promise<CombinedData[]> {
const from = (page - 1) * limit; const from = (page - 1) * limit;
const to = from + limit - 1; 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) { if (!entries || entries.length === 0) {
return []; return [];
@@ -256,13 +344,25 @@ class CombinedDataRepository {
return combinedData; 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 }); let query = this.supabase.from('pokedex_entries').select('id', { count: 'exact', head: true });
// Apply same filters as in findCombinedData // Apply same filters as in findCombinedData
if (!enableForms) { if (!enableForms) {
// Filter to only base forms (form is NULL) OR Unown "A" (since Unown has no base form) // Filter to base forms: NULL or 'male' (gendered species), plus Unown "A".
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)');
} }
if (region) { 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'; import type { SupabaseClient } from '@supabase/supabase-js';
class PokedexEntryRepository { class PokedexEntryRepository {
constructor(private supabase: SupabaseClient) {} 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) // Transform Supabase data to match frontend expectations (minimal transformation)
private transformPokedexEntry(entry: PokedexEntryDB): PokedexEntry { private transformPokedexEntry(entry: PokedexEntryDB): PokedexEntry {
return { return {
@@ -16,7 +52,7 @@ class PokedexEntryRepository {
gamesToCatchIn: entry.gamesToCatchIn || [], gamesToCatchIn: entry.gamesToCatchIn || [],
regionToEvolveIn: entry.regionToEvolveIn || '', regionToEvolveIn: entry.regionToEvolveIn || '',
evolutionInformation: entry.evolutionInformation || '', 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.regionToEvolveIn !== undefined) dbData.regionToEvolveIn = data.regionToEvolveIn;
if (data.evolutionInformation !== undefined) if (data.evolutionInformation !== undefined)
dbData.evolutionInformation = data.evolutionInformation; 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 // Box placement is calculated dynamically - not stored in database
const { data: result, error } = await this.supabase const { data: result, error } = await this.supabase
@@ -78,7 +116,9 @@ class PokedexEntryRepository {
if (data.regionToEvolveIn !== undefined) dbData.regionToEvolveIn = data.regionToEvolveIn; if (data.regionToEvolveIn !== undefined) dbData.regionToEvolveIn = data.regionToEvolveIn;
if (data.evolutionInformation !== undefined) if (data.evolutionInformation !== undefined)
dbData.evolutionInformation = data.evolutionInformation; 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 // Box placement is calculated dynamically - not stored in database
const { data: result, error } = await this.supabase const { data: result, error } = await this.supabase
+24 -4
View File
@@ -7,7 +7,7 @@ class PokedexRepository {
private userId: string private userId: string
) {} ) {}
private transform(db: PokedexDB): Pokedex { private transform(db: PokedexDB, dexScopes: string[] = []): Pokedex {
return { return {
_id: db.id, _id: db.id,
userId: db.userId, userId: db.userId,
@@ -17,7 +17,8 @@ class PokedexRepository {
isShinyDex: db.isShinyDex, isShinyDex: db.isShinyDex,
isOriginDex: db.isOriginDex, isOriginDex: db.isOriginDex,
isFormDex: db.isFormDex, isFormDex: db.isFormDex,
gameScope: db.gameScope gameScope: db.gameScope,
dexScopes
}; };
} }
@@ -33,6 +34,23 @@ class PokedexRepository {
return dbData; 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> { async findById(id: string): Promise<Pokedex | null> {
const { data, error } = await this.supabase const { data, error } = await this.supabase
.from('pokedexes') .from('pokedexes')
@@ -42,7 +60,8 @@ class PokedexRepository {
.single(); .single();
if (error || !data) return null; 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[]> { async findAll(): Promise<Pokedex[]> {
@@ -53,7 +72,8 @@ class PokedexRepository {
.order('createdAt', { ascending: true }); .order('createdAt', { ascending: true });
if (error || !data) return []; 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> { 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 { class RegionGameMappingRepository {
async findById(id: string): Promise<RegionGameMapping | null> { async findById(id: string): Promise<RegionGameMapping | null> {
return RegionGameMappingModel.findById(id).exec(); void id;
return null;
} }
async findAll(): Promise<RegionGameMapping[]> { async findAll(): Promise<RegionGameMapping[]> {
return RegionGameMappingModel.find().exec(); return [];
} }
async create(data: Partial<RegionGameMapping>): Promise<RegionGameMapping> { 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> { 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> { 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 { Pokedex } from '$lib/models/Pokedex';
import type { SupabaseClient } from '@supabase/supabase-js'; import type { SupabaseClient } from '@supabase/supabase-js';
import { resolveDexScopes } from '$lib/services/PokedexDexScopeService';
/** /**
* Calculate expected pokedex entries based on pokedex configuration * Calculate expected pokedex entries based on pokedex configuration
@@ -10,12 +11,54 @@ export async function calculateExpectedEntries(
supabase: SupabaseClient, supabase: SupabaseClient,
pokedex: Pokedex pokedex: Pokedex
): Promise<number[]> { ): 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'); let query = supabase.from('pokedex_entries').select('id');
// Apply form filter: if isFormDex is false, only include base forms // 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') // Base forms have form IS NULL, except Unown which has no base form (use 'A')
if (!pokedex.isFormDex) { 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 // 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'; import { pwaAssetsHead } from 'virtual:pwa-assets/head';
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : ''; $: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '';
const pwaDescription = (pwaAssetsHead as { description?: { content: string } }).description;
export let data; export let data;
let { supabase } = data; let { supabase } = data;
@@ -22,8 +23,10 @@
}); });
onDestroy(unsubscribe); onDestroy(unsubscribe);
onMount(async () => { let authSubscription: { unsubscribe: () => void } | null = null;
await getUser();
onMount(() => {
void getUser();
// Listen for auth state changes to keep the user store in sync // Listen for auth state changes to keep the user store in sync
const { const {
@@ -36,27 +39,30 @@
} }
user.set(localUser); user.set(localUser);
}); });
authSubscription = subscription;
if (pwaInfo) { if (pwaInfo) {
const { registerSW } = await import('virtual:pwa-register'); void (async () => {
registerSW({ const { registerSW } = await import('virtual:pwa-register');
immediate: true, registerSW({
onRegistered(r) { immediate: true,
// uncomment following code if you want check for updates onRegistered(r) {
// r && setInterval(() => { // uncomment following code if you want check for updates
// console.log('Checking for sw update') // r && setInterval(() => {
// r.update() // console.log('Checking for sw update')
// }, 20000 /* 20s for testing purposes */) // r.update()
console.log(`SW Registered: ${r}`); // }, 20000 /* 20s for testing purposes */)
}, console.log(`SW Registered: ${r}`);
onRegisterError(error) { },
console.log('SW registration error', error); onRegisterError(error) {
} console.log('SW registration error', error);
}); }
});
})();
} }
return () => { return () => {
subscription.unsubscribe(); authSubscription?.unsubscribe();
}; };
}); });
@@ -80,8 +86,8 @@
{#if pwaAssetsHead.themeColor} {#if pwaAssetsHead.themeColor}
<meta name="theme-color" content={pwaAssetsHead.themeColor.content} /> <meta name="theme-color" content={pwaAssetsHead.themeColor.content} />
{/if} {/if}
{#if pwaAssetsHead.description} {#if pwaDescription}
<meta name="description" content={pwaAssetsHead.description.content} /> <meta name="description" content={pwaDescription.content} />
{/if} {/if}
{#each pwaAssetsHead.links as link} {#each pwaAssetsHead.links as link}
<link {...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 { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { populatePokedexMappings } from '$lib/services/PokedexMappingService'; import { populatePokedexMappings } from '$lib/services/PokedexMappingService';
import { resolveDexScopes, setPokedexDexScopes } from '$lib/services/PokedexDexScopeService';
// GET: List all pokedexes for user // GET: List all pokedexes for user
export const GET = async (event: RequestEvent) => { export const GET = async (event: RequestEvent) => {
@@ -31,12 +32,20 @@ export const POST = async (event: RequestEvent) => {
const data: Partial<Pokedex> = await event.request.json(); const data: Partial<Pokedex> = await event.request.json();
requestedName = typeof data.name === 'string' ? data.name : undefined; requestedName = typeof data.name === 'string' ? data.name : undefined;
data.userId = userId; data.userId = userId;
const requestedDexScopes = Array.isArray(data.dexScopes) ? data.dexScopes : [];
const repo = new PokedexRepository(event.locals.supabase, userId); const repo = new PokedexRepository(event.locals.supabase, userId);
const pokedex = await repo.create(data); const pokedex = await repo.create(data);
createdPokedexId = pokedex._id; createdPokedexId = pokedex._id;
createdPokedexName = pokedex.name; 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 // Populate pokedex_entries_mapping table with expected entries
try { try {
await populatePokedexMappings(event.locals.supabase, pokedex._id, pokedex); 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 { requireAuth } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
import { recalculatePokedexMappings } from '$lib/services/PokedexMappingService'; import { recalculatePokedexMappings } from '$lib/services/PokedexMappingService';
import { resolveDexScopes, setPokedexDexScopes } from '$lib/services/PokedexDexScopeService';
// GET: Get single pokedex by ID // GET: Get single pokedex by ID
export const GET = async (event: RequestEvent) => { export const GET = async (event: RequestEvent) => {
@@ -71,12 +72,35 @@ export const PUT = async (event: RequestEvent) => {
return json({ error: 'Pokedex not found' }, { status: 404 }); 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 // 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 // Compare the persisted result (pokedex) to existingPokedex to catch changes that repo.update may normalize or default
const configChanged = const configChanged =
pokedex.isFormDex !== existingPokedex.isFormDex || pokedex.isFormDex !== existingPokedex.isFormDex ||
pokedex.gameScope !== existingPokedex.gameScope; pokedex.gameScope !== existingPokedex.gameScope ||
dexScopesChanged;
if (configChanged) { if (configChanged) {
await recalculatePokedexMappings(event.locals.supabase, id, pokedex); await recalculatePokedexMappings(event.locals.supabase, id, pokedex);
@@ -3,6 +3,7 @@ import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
import PokedexRepository from '$lib/repositories/PokedexRepository'; import PokedexRepository from '$lib/repositories/PokedexRepository';
import { getOptionalUserId } from '$lib/utils/auth'; import { getOptionalUserId } from '$lib/utils/auth';
import type { RequestEvent } from '@sveltejs/kit'; 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 // GET: Get combined data (pokédex entries + catch records) for specific pokédex
export const GET = async (event: RequestEvent) => { 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 // Use pokédex's gameScope as default filter if no manual game filter is set
const effectiveGame = game || pokedex.gameScope || ''; const effectiveGame = game || pokedex.gameScope || '';
const dexScopes = await resolveDexScopes(event.locals.supabase, pokedex);
const repo = new CombinedDataRepository(event.locals.supabase, userId, pokedexId); const repo = new CombinedDataRepository(event.locals.supabase, userId, pokedexId);
@@ -49,11 +51,12 @@ export const GET = async (event: RequestEvent) => {
limit, limit,
enableForms, enableForms,
region, region,
effectiveGame effectiveGame,
dexScopes
); );
// Get total count for pagination // 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); const totalPages = Math.ceil(totalCount / limit);
return json({ return json({
+2 -1
View File
@@ -146,11 +146,12 @@
<!-- Email Input --> <!-- Email Input -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="forgot-password-email">
<span class="label-text font-medium">Email</span> <span class="label-text font-medium">Email</span>
</label> </label>
<div class="relative"> <div class="relative">
<input <input
id="forgot-password-email"
type="email" type="email"
placeholder="your@email.com" placeholder="your@email.com"
class="input input-bordered w-full pl-10" class="input input-bordered w-full pl-10"
+10 -3
View File
@@ -15,7 +15,8 @@
isShinyDex: false, isShinyDex: false,
isOriginDex: false, isOriginDex: false,
isFormDex: false, isFormDex: false,
gameScope: null gameScope: null,
dexScopes: []
}; };
$: mode = editingPokedex ? ('edit' as const) : ('create' as const); $: mode = editingPokedex ? ('edit' as const) : ('create' as const);
@@ -55,7 +56,8 @@
isShinyDex: false, isShinyDex: false,
isOriginDex: false, isOriginDex: false,
isFormDex: false, isFormDex: false,
gameScope: null gameScope: null,
dexScopes: []
}; };
showModal = true; showModal = true;
} }
@@ -199,6 +201,11 @@
</h3> </h3>
<PokedexForm bind:pokedex={formData} {mode} onSubmit={handleSubmit} onCancel={closeModal} /> <PokedexForm bind:pokedex={formData} {mode} onSubmit={handleSubmit} onCancel={closeModal} />
</div> </div>
<div class="modal-backdrop" on:click={closeModal}></div> <button
type="button"
class="modal-backdrop"
aria-label="Close modal"
on:click={closeModal}
></button>
</div> </div>
{/if} {/if}
+5
View File
@@ -467,6 +467,11 @@
/> />
</svg> </svg>
<span class="font-semibold">{pokedex.gameScope}</span> <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> </div>
{:else} {:else}
<div class="divider divider-horizontal mx-0"></div> <div class="divider divider-horizontal mx-0"></div>
+4 -2
View File
@@ -174,11 +174,12 @@
<!-- New Password Input --> <!-- New Password Input -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="reset-password-new">
<span class="label-text font-medium">New Password</span> <span class="label-text font-medium">New Password</span>
</label> </label>
<div class="relative"> <div class="relative">
<input <input
id="reset-password-new"
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
class="input input-bordered w-full pl-10" class="input input-bordered w-full pl-10"
@@ -203,11 +204,12 @@
<!-- Confirm Password Input --> <!-- Confirm Password Input -->
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label" for="reset-password-confirm">
<span class="label-text font-medium">Confirm Password</span> <span class="label-text font-medium">Confirm Password</span>
</label> </label>
<div class="relative"> <div class="relative">
<input <input
id="reset-password-confirm"
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
class="input input-bordered w-full pl-10" 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_region_id ON games("regionId");
CREATE INDEX idx_games_release_year ON games("releaseYear"); 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) -- Reference data: pokemon (one row per pokedexNumber + form)
CREATE TABLE pokemon ( CREATE TABLE pokemon (
id BIGSERIAL PRIMARY KEY, 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) -- Reference data: per-game pokedex entries (seeded from per-game CSVs)
CREATE TABLE game_pokedex_entries ( 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, "pokemonId" BIGINT NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
"dexNumber" INTEGER, "dexNumber" INTEGER,
notes TEXT, 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"); CREATE INDEX idx_game_pokedex_entries_pokemon_id ON game_pokedex_entries("pokemonId");
-- User-owned pokedexes -- User-owned pokedexes
@@ -108,6 +124,16 @@ CREATE TABLE pokedexes (
CREATE INDEX idx_pokedexes_user_id ON pokedexes("userId"); 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 -- User-owned expected entries per pokedex
CREATE TABLE pokedex_pokemon_mapping ( CREATE TABLE pokedex_pokemon_mapping (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@@ -167,6 +193,10 @@ CREATE TRIGGER update_games_updated_at
BEFORE UPDATE ON games BEFORE UPDATE ON games
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); 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 CREATE TRIGGER update_pokemon_updated_at
BEFORE UPDATE ON pokemon BEFORE UPDATE ON pokemon
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); 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 CREATE POLICY "Games are publicly readable" ON games
FOR SELECT TO public USING (true); 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; ALTER TABLE pokemon ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Pokemon are publicly readable" ON pokemon CREATE POLICY "Pokemon are publicly readable" ON pokemon
FOR SELECT TO public USING (true); 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 CREATE POLICY "Users can delete own pokedexes" ON pokedexes
FOR DELETE USING (auth.uid() = "userId"); 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; ALTER TABLE pokedex_pokemon_mapping ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view pokedex pokemon mapping" CREATE POLICY "Anyone can view pokedex pokemon mapping"
ON 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; 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 -- RPC: recalculate expected mappings for a pokedex
CREATE OR REPLACE FUNCTION recalculate_pokedex_mappings( CREATE OR REPLACE FUNCTION recalculate_pokedex_mappings(
p_pokedex_id UUID, 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" "virtual:pwa-assets"
] ]
}, },
"include": ["src/**/*.ts", "src/**/*.svelte", "node_modules/vite-plugin-pwa/client.d.ts"], "include": [
"exclude": [".svelte-kit/**/*", "node_modules/**/*"] ".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 // 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 // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes