mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-15 03:53:43 +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 -->
|
<!-- 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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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
@@ -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} />
|
||||||
|
|||||||
@@ -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 { 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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user