mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-12 18:43:45 +00:00
data(*): Add final data changes
This commit is contained in:
@@ -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
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Pokedex {
|
||||
isOriginDex: boolean;
|
||||
isFormDex: boolean;
|
||||
gameScope: string | null;
|
||||
dexScopes: string[];
|
||||
}
|
||||
|
||||
export interface PokedexDB {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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} />
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user