mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-15 12:03:44 +00:00
feat(#67): Add support for multiple pokedexes per user
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Pokedex } from '$lib/models/Pokedex';
|
||||||
|
|
||||||
|
export let pokedex: Pokedex;
|
||||||
|
export let onEdit: () => void;
|
||||||
|
export let onDelete: () => void;
|
||||||
|
export let onView: () => void;
|
||||||
|
|
||||||
|
// Get active type badges
|
||||||
|
$: typeBadges = [
|
||||||
|
pokedex.isLivingDex && 'Living',
|
||||||
|
pokedex.isShinyDex && 'Shiny',
|
||||||
|
pokedex.isOriginDex && 'Origin',
|
||||||
|
pokedex.isFormDex && 'Form'
|
||||||
|
].filter(Boolean);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">
|
||||||
|
{pokedex.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{#if pokedex.description}
|
||||||
|
<p class="text-sm">{pokedex.description}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 my-2">
|
||||||
|
{#each typeBadges as badge}
|
||||||
|
<span class="badge badge-primary">{badge}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if pokedex.gameScope}
|
||||||
|
<div class="text-sm text-base-content/70">
|
||||||
|
<span class="font-semibold">Game:</span>
|
||||||
|
{pokedex.gameScope}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-sm text-base-content/70">
|
||||||
|
<span class="font-semibold">Scope:</span> All Games
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="card-actions justify-end mt-4">
|
||||||
|
<button class="btn btn-sm btn-ghost" on:click={onEdit}>Edit</button>
|
||||||
|
<button class="btn btn-sm btn-error btn-outline" on:click={onDelete}>Delete</button>
|
||||||
|
<button class="btn btn-sm btn-primary" on:click={onView}>View</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
export let showForms: boolean;
|
export let showForms: boolean;
|
||||||
export let showShiny: boolean;
|
export let showShiny: boolean;
|
||||||
export let userId: string | null = null;
|
export let userId: string | null = null;
|
||||||
|
export let pokedexId: string;
|
||||||
|
|
||||||
// Create a default catch record if none exists
|
// Create a default catch record if none exists
|
||||||
$: if (!catchRecord) {
|
$: if (!catchRecord) {
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
_id: '', // Empty string, not temp ID - will be created by server
|
_id: '', // Empty string, not temp ID - will be created by server
|
||||||
userId: userId || '',
|
userId: userId || '',
|
||||||
pokedexEntryId: pokedexEntry._id,
|
pokedexEntryId: pokedexEntry._id,
|
||||||
|
pokedexId: pokedexId,
|
||||||
haveToEvolve: false,
|
haveToEvolve: false,
|
||||||
caught: false,
|
caught: false,
|
||||||
inHome: false,
|
inHome: false,
|
||||||
@@ -83,74 +85,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dex-column catch-record-container bg-white text-black rounded-lg p-4 mb-4 md:mb-0">
|
{#if catchRecord}
|
||||||
<div class="flex items-center">
|
<div class="dex-column catch-record-container bg-white text-black rounded-lg p-4 mb-4 md:mb-0">
|
||||||
<div class="form-control">
|
|
||||||
<label class="cursor-pointer label">
|
|
||||||
<span class="block font-bold mr-2">Caught:</span>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={catchRecord.caught}
|
|
||||||
class="checkbox checkbox-primary border-black"
|
|
||||||
on:change={updateCatchRecord}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="cursor-pointer label">
|
|
||||||
<span class="block font-bold mr-2">Needs to evolve:</span>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={catchRecord.haveToEvolve}
|
|
||||||
class="checkbox checkbox-primary border-black"
|
|
||||||
on:change={updateCatchRecord}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="cursor-pointer label">
|
|
||||||
<span class="block font-bold mr-2">In home:</span>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={catchRecord.inHome}
|
|
||||||
class="checkbox checkbox-primary border-black"
|
|
||||||
on:change={updateCatchRecord}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if pokedexEntry.canGigantamax && showForms}
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="cursor-pointer label">
|
<label class="cursor-pointer label">
|
||||||
<span class="block font-bold mr-2">Has Gigantamaxed:</span>
|
<span class="block font-bold mr-2">Caught:</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={catchRecord.hasGigantamaxed}
|
bind:checked={catchRecord.caught}
|
||||||
class="checkbox checkbox-primary border-black"
|
class="checkbox checkbox-primary border-black"
|
||||||
on:change={updateCatchRecord}
|
on:change={updateCatchRecord}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div class="flex items-center">
|
||||||
<p>
|
<div class="form-control">
|
||||||
<label
|
<label class="cursor-pointer label">
|
||||||
class="block font-bold mb-1"
|
<span class="block font-bold mr-2">Needs to evolve:</span>
|
||||||
for={`personalNotesInput-${catchRecord?._id || pokedexEntry._id}`}>Notes:</label
|
<input
|
||||||
>
|
type="checkbox"
|
||||||
<textarea
|
bind:checked={catchRecord.haveToEvolve}
|
||||||
bind:value={catchRecord.personalNotes}
|
class="checkbox checkbox-primary border-black"
|
||||||
id={`personalNotesInput-${catchRecord?._id || pokedexEntry._id}`}
|
on:change={updateCatchRecord}
|
||||||
class="form-textarea w-full p-2 border rounded"
|
/>
|
||||||
on:change={updateCatchRecord}
|
</label>
|
||||||
></textarea>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div class="flex items-center">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="cursor-pointer label">
|
||||||
|
<span class="block font-bold mr-2">In home:</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={catchRecord.inHome}
|
||||||
|
class="checkbox checkbox-primary border-black"
|
||||||
|
on:change={updateCatchRecord}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if pokedexEntry.canGigantamax && showForms}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="cursor-pointer label">
|
||||||
|
<span class="block font-bold mr-2">Has Gigantamaxed:</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={catchRecord.hasGigantamaxed}
|
||||||
|
class="checkbox checkbox-primary border-black"
|
||||||
|
on:change={updateCatchRecord}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<p>
|
||||||
|
<label
|
||||||
|
class="block font-bold mb-1"
|
||||||
|
for={`personalNotesInput-${catchRecord._id || pokedexEntry._id}`}>Notes:</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
bind:value={catchRecord.personalNotes}
|
||||||
|
id={`personalNotesInput-${catchRecord._id || pokedexEntry._id}`}
|
||||||
|
class="form-textarea w-full p-2 border rounded"
|
||||||
|
on:change={updateCatchRecord}
|
||||||
|
></textarea>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="dex-column additional-details-container">
|
<div class="dex-column additional-details-container">
|
||||||
{#if showOrigins}
|
{#if showOrigins}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Pokedex } from '$lib/models/Pokedex';
|
||||||
|
|
||||||
|
export let pokedex: Partial<Pokedex> = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
isLivingDex: false,
|
||||||
|
isShinyDex: false,
|
||||||
|
isOriginDex: false,
|
||||||
|
isFormDex: false,
|
||||||
|
gameScope: null
|
||||||
|
};
|
||||||
|
export let mode: 'create' | 'edit' = 'create';
|
||||||
|
export let onSubmit: () => void;
|
||||||
|
export let onCancel: () => void;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
$: hasAtLeastOneType =
|
||||||
|
pokedex.isLivingDex || pokedex.isShinyDex || pokedex.isOriginDex || pokedex.isFormDex;
|
||||||
|
$: canSubmit = pokedex.name && pokedex.name.trim() !== '' && hasAtLeastOneType;
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (canSubmit) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="pokedex-name">
|
||||||
|
<span class="label-text">Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="pokedex-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="My Living Dex"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={pokedex.name}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="pokedex-description">
|
||||||
|
<span class="label-text">Description (optional)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="pokedex-description"
|
||||||
|
class="textarea textarea-bordered h-24"
|
||||||
|
placeholder="Describe your pokédex..."
|
||||||
|
bind:value={pokedex.description}
|
||||||
|
></textarea>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="game-scope">
|
||||||
|
<span class="label-text">Game Scope</span>
|
||||||
|
</label>
|
||||||
|
<select id="game-scope" class="select select-bordered" bind:value={pokedex.gameScope}>
|
||||||
|
<option value={null}>All Games</option>
|
||||||
|
<option value="Scarlet">Scarlet</option>
|
||||||
|
<option value="Violet">Violet>
|
||||||
|
<option value="Sword">Sword</option>
|
||||||
|
<option value="Shield">Shield</option>
|
||||||
|
<option value="Brilliant Diamond">Brilliant Diamond</option>
|
||||||
|
<option value="Shining Pearl">Shining Pearl</option>
|
||||||
|
<option value="Legends: Arceus">Legends: Arceus</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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}>
|
||||||
|
{mode === 'create' ? 'Create' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Pagination from '$lib/components/Pagination.svelte';
|
import Pagination from '$lib/components/Pagination.svelte';
|
||||||
|
import type { Pokedex } from '$lib/models/Pokedex';
|
||||||
|
|
||||||
|
export let pokedex: Pokedex | null = null;
|
||||||
export let viewAsBoxes = false;
|
export let viewAsBoxes = false;
|
||||||
export let currentPage = 1;
|
export let currentPage = 1;
|
||||||
export let itemsPerPage = 20;
|
export let itemsPerPage = 20;
|
||||||
@@ -15,11 +17,36 @@
|
|||||||
export let toggleShiny = () => {};
|
export let toggleShiny = () => {};
|
||||||
export let getData = () => {};
|
export let getData = () => {};
|
||||||
export let toggleViewAsBoxes = () => {};
|
export let toggleViewAsBoxes = () => {};
|
||||||
|
|
||||||
|
// Get active type badges
|
||||||
|
$: typeBadges = pokedex
|
||||||
|
? [
|
||||||
|
pokedex.isLivingDex && 'Living',
|
||||||
|
pokedex.isShinyDex && 'Shiny',
|
||||||
|
pokedex.isOriginDex && 'Origin',
|
||||||
|
pokedex.isFormDex && 'Form'
|
||||||
|
].filter(Boolean)
|
||||||
|
: [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class="menu bg-gray-800 text-white min-h-full w-64 p-4 lg:fixed xs:top-0 lg:left-0 h-full lg:h-5/6"
|
class="menu bg-gray-800 text-white min-h-full w-64 p-4 lg:fixed xs:top-0 lg:left-0 h-full lg:h-5/6"
|
||||||
>
|
>
|
||||||
|
<!-- Pokédex Info Section -->
|
||||||
|
{#if pokedex}
|
||||||
|
<div class="mb-6 pb-4 border-b border-gray-700">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">{pokedex.name}</h2>
|
||||||
|
<div class="flex flex-wrap gap-1 mb-2">
|
||||||
|
{#each typeBadges as badge}
|
||||||
|
<span class="badge badge-sm badge-primary">{badge}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if pokedex.gameScope}
|
||||||
|
<p class="text-sm text-gray-400">Game: {pokedex.gameScope}</p>
|
||||||
|
{/if}
|
||||||
|
<a href="/my-pokedexes" class="btn btn-ghost btn-sm mt-2 w-full"> Manage Pokédexes </a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<h2 class="text-2xl font-semibold mb-4">Views</h2>
|
<h2 class="text-2xl font-semibold mb-4">Views</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
|
||||||
export let showShiny = false;
|
export let showShiny = false;
|
||||||
|
export let showForms = true;
|
||||||
export let combinedData: CombinedData[] | null;
|
export let combinedData: CombinedData[] | null;
|
||||||
export let boxNumbers = Array<number>;
|
export let boxNumbers: number[] = [];
|
||||||
export let currentPlacement = 'boxPlacementForms';
|
export let currentPlacement = 'boxPlacementForms';
|
||||||
export let creatingRecords = false;
|
export let creatingRecords = false;
|
||||||
export let totalRecordsCreated = 0;
|
export let totalRecordsCreated = 0;
|
||||||
@@ -33,7 +34,8 @@
|
|||||||
if (catchRecord?.caught || catchRecord?.haveToEvolve) {
|
if (catchRecord?.caught || catchRecord?.haveToEvolve) {
|
||||||
return '';
|
return '';
|
||||||
} else {
|
} else {
|
||||||
if (pokedexEntry[currentPlacement].column % 2 === 0) {
|
const placement = showForms ? pokedexEntry.boxPlacementForms : pokedexEntry.boxPlacement;
|
||||||
|
if (placement.column % 2 === 0) {
|
||||||
return 'background-color: #ffffff';
|
return 'background-color: #ffffff';
|
||||||
} else {
|
} else {
|
||||||
return 'background-color: #f9f9f9;';
|
return 'background-color: #f9f9f9;';
|
||||||
@@ -67,11 +69,13 @@
|
|||||||
>
|
>
|
||||||
<div class="grid grid-cols-6">
|
<div class="grid grid-cols-6">
|
||||||
{#each combinedData as { pokedexEntry, catchRecord }}
|
{#each combinedData as { pokedexEntry, catchRecord }}
|
||||||
{#if pokedexEntry[currentPlacement].box === boxNumber}
|
{@const placement = showForms
|
||||||
|
? pokedexEntry.boxPlacementForms
|
||||||
|
: pokedexEntry.boxPlacement}
|
||||||
|
{#if placement.box === boxNumber}
|
||||||
<div
|
<div
|
||||||
class="pokemon-box {cellBackgroundColourClass(catchRecord)}"
|
class="pokemon-box {cellBackgroundColourClass(catchRecord)}"
|
||||||
style="grid-column-start: {pokedexEntry[currentPlacement]
|
style="grid-column-start: {placement.column}; grid-row-start: {placement.row};
|
||||||
.column}; grid-row-start: {pokedexEntry[currentPlacement].row};
|
|
||||||
{cellBackgroundColourStyle(pokedexEntry, catchRecord)}"
|
{cellBackgroundColourStyle(pokedexEntry, catchRecord)}"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
export let updateACatch = (event: any) => {};
|
export let updateACatch = (event: any) => {};
|
||||||
export let createCatchRecords = () => {};
|
export let createCatchRecords = () => {};
|
||||||
export let userId: string | null = null;
|
export let userId: string | null = null;
|
||||||
|
export let pokedexId: string;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="flex-1 p-4 w-full">
|
<main class="flex-1 p-4 w-full">
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
{showForms}
|
{showForms}
|
||||||
{showShiny}
|
{showShiny}
|
||||||
{userId}
|
{userId}
|
||||||
|
{pokedexId}
|
||||||
bind:catchRecord
|
bind:catchRecord
|
||||||
on:updateCatch={updateACatch}
|
on:updateCatch={updateACatch}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface CatchRecord {
|
|||||||
_id: string; // Maps to Supabase 'id' field for frontend compatibility
|
_id: string; // Maps to Supabase 'id' field for frontend compatibility
|
||||||
userId: string;
|
userId: string;
|
||||||
pokedexEntryId: string; // String representation of the foreign key
|
pokedexEntryId: string; // String representation of the foreign key
|
||||||
|
pokedexId: string; // NEW: Foreign key to pokedexes table
|
||||||
haveToEvolve: boolean;
|
haveToEvolve: boolean;
|
||||||
caught: boolean;
|
caught: boolean;
|
||||||
inHome: boolean;
|
inHome: boolean;
|
||||||
@@ -15,6 +16,7 @@ export interface CatchRecordDB {
|
|||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
pokedexEntryId: number; // Numeric foreign key in database
|
pokedexEntryId: number; // Numeric foreign key in database
|
||||||
|
pokedexId: string; // NEW: Foreign key to pokedexes table
|
||||||
haveToEvolve: boolean;
|
haveToEvolve: boolean;
|
||||||
caught: boolean;
|
caught: boolean;
|
||||||
inHome: boolean;
|
inHome: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export interface Pokedex {
|
||||||
|
_id: string;
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isLivingDex: boolean;
|
||||||
|
isShinyDex: boolean;
|
||||||
|
isOriginDex: boolean;
|
||||||
|
isFormDex: boolean;
|
||||||
|
gameScope: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PokedexDB {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isLivingDex: boolean;
|
||||||
|
isShinyDex: boolean;
|
||||||
|
isOriginDex: boolean;
|
||||||
|
isFormDex: boolean;
|
||||||
|
gameScope: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
@@ -12,6 +12,23 @@ export interface PokedexEntry {
|
|||||||
regionToEvolveIn: string;
|
regionToEvolveIn: string;
|
||||||
evolutionInformation: string;
|
evolutionInformation: string;
|
||||||
catchInformation: string[];
|
catchInformation: string[];
|
||||||
|
// Regional dex numbers
|
||||||
|
kantoDexNumber?: number;
|
||||||
|
johtoDexNumber?: number;
|
||||||
|
hoennDexNumber?: number;
|
||||||
|
sinnohDexNumber?: number;
|
||||||
|
unovaBwDexNumber?: number;
|
||||||
|
unovaB2w2DexNumber?: number;
|
||||||
|
kalosCentralDexNumber?: number;
|
||||||
|
kalosCoastalDexNumber?: number;
|
||||||
|
kalosMountainDexNumber?: number;
|
||||||
|
alolaSmDexNumber?: number;
|
||||||
|
alolaUsumDexNumber?: number;
|
||||||
|
galarDexNumber?: number;
|
||||||
|
galarIsleOfArmorDexNumber?: number;
|
||||||
|
galarCrownTundraDexNumber?: number;
|
||||||
|
hisuiDexNumber?: number;
|
||||||
|
paldeaDexNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database record type for Supabase
|
// Database record type for Supabase
|
||||||
@@ -32,6 +49,23 @@ export interface PokedexEntryDB {
|
|||||||
boxPlacementBox: number | null;
|
boxPlacementBox: number | null;
|
||||||
boxPlacementRow: number | null;
|
boxPlacementRow: number | null;
|
||||||
boxPlacementColumn: number | null;
|
boxPlacementColumn: number | null;
|
||||||
|
// Regional dex numbers - snake_case to match database
|
||||||
|
kanto_dex_number: number | null;
|
||||||
|
johto_dex_number: number | null;
|
||||||
|
hoenn_dex_number: number | null;
|
||||||
|
sinnoh_dex_number: number | null;
|
||||||
|
unova_bw_dex_number: number | null;
|
||||||
|
unova_b2w2_dex_number: number | null;
|
||||||
|
kalos_central_dex_number: number | null;
|
||||||
|
kalos_coastal_dex_number: number | null;
|
||||||
|
kalos_mountain_dex_number: number | null;
|
||||||
|
alola_sm_dex_number: number | null;
|
||||||
|
alola_usum_dex_number: number | null;
|
||||||
|
galar_dex_number: number | null;
|
||||||
|
galar_isle_of_armor_dex_number: number | null;
|
||||||
|
galar_crown_tundra_dex_number: number | null;
|
||||||
|
hisui_dex_number: number | null;
|
||||||
|
paldea_dex_number: number | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
|||||||
class CatchRecordRepository {
|
class CatchRecordRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private supabase: SupabaseClient,
|
private supabase: SupabaseClient,
|
||||||
private userId: string
|
private userId: string,
|
||||||
|
private pokedexId: string
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Transform Supabase data to frontend format (minimal transformation)
|
// Transform Supabase data to frontend format (minimal transformation)
|
||||||
@@ -13,6 +14,7 @@ class CatchRecordRepository {
|
|||||||
_id: record.id,
|
_id: record.id,
|
||||||
userId: record.userId,
|
userId: record.userId,
|
||||||
pokedexEntryId: record.pokedexEntryId.toString(),
|
pokedexEntryId: record.pokedexEntryId.toString(),
|
||||||
|
pokedexId: record.pokedexId,
|
||||||
haveToEvolve: record.haveToEvolve,
|
haveToEvolve: record.haveToEvolve,
|
||||||
caught: record.caught,
|
caught: record.caught,
|
||||||
inHome: record.inHome,
|
inHome: record.inHome,
|
||||||
@@ -31,6 +33,7 @@ class CatchRecordRepository {
|
|||||||
}
|
}
|
||||||
dbData.pokedexEntryId = numericId;
|
dbData.pokedexEntryId = numericId;
|
||||||
}
|
}
|
||||||
|
if (data.pokedexId !== undefined) dbData.pokedexId = data.pokedexId;
|
||||||
if (data.haveToEvolve !== undefined) dbData.haveToEvolve = data.haveToEvolve;
|
if (data.haveToEvolve !== undefined) dbData.haveToEvolve = data.haveToEvolve;
|
||||||
if (data.caught !== undefined) dbData.caught = data.caught;
|
if (data.caught !== undefined) dbData.caught = data.caught;
|
||||||
if (data.inHome !== undefined) dbData.inHome = data.inHome;
|
if (data.inHome !== undefined) dbData.inHome = data.inHome;
|
||||||
@@ -46,6 +49,7 @@ class CatchRecordRepository {
|
|||||||
.select('*')
|
.select('*')
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.eq('userId', this.userId)
|
.eq('userId', this.userId)
|
||||||
|
.eq('pokedexId', this.pokedexId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error || !data) return null;
|
if (error || !data) return null;
|
||||||
@@ -56,7 +60,8 @@ class CatchRecordRepository {
|
|||||||
const { data, error } = await this.supabase
|
const { data, error } = await this.supabase
|
||||||
.from('catch_records')
|
.from('catch_records')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('userId', this.userId);
|
.eq('userId', this.userId)
|
||||||
|
.eq('pokedexId', this.pokedexId);
|
||||||
|
|
||||||
if (error || !data) return [];
|
if (error || !data) return [];
|
||||||
return data.map((record) => this.transformCatchRecord(record));
|
return data.map((record) => this.transformCatchRecord(record));
|
||||||
@@ -66,7 +71,8 @@ class CatchRecordRepository {
|
|||||||
const { data, error } = await this.supabase
|
const { data, error } = await this.supabase
|
||||||
.from('catch_records')
|
.from('catch_records')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('userId', userId);
|
.eq('userId', userId)
|
||||||
|
.eq('pokedexId', this.pokedexId);
|
||||||
|
|
||||||
if (error || !data) return [];
|
if (error || !data) return [];
|
||||||
return data.map((record) => this.transformCatchRecord(record));
|
return data.map((record) => this.transformCatchRecord(record));
|
||||||
@@ -75,6 +81,7 @@ class CatchRecordRepository {
|
|||||||
async create(data: Partial<CatchRecord>): Promise<CatchRecord> {
|
async create(data: Partial<CatchRecord>): Promise<CatchRecord> {
|
||||||
const dbData = {
|
const dbData = {
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
|
pokedexId: this.pokedexId,
|
||||||
...this.transformToDatabase(data)
|
...this.transformToDatabase(data)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,6 +111,7 @@ class CatchRecordRepository {
|
|||||||
.update(dbData)
|
.update(dbData)
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.eq('userId', this.userId)
|
.eq('userId', this.userId)
|
||||||
|
.eq('pokedexId', this.pokedexId)
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
@@ -116,7 +124,8 @@ class CatchRecordRepository {
|
|||||||
.from('catch_records')
|
.from('catch_records')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.eq('userId', this.userId);
|
.eq('userId', this.userId)
|
||||||
|
.eq('pokedexId', this.pokedexId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error deleting catch record:', error);
|
console.error('Error deleting catch record:', error);
|
||||||
@@ -128,8 +137,8 @@ class CatchRecordRepository {
|
|||||||
if (data._id) {
|
if (data._id) {
|
||||||
return this.update(data._id, data);
|
return this.update(data._id, data);
|
||||||
} else if (data.pokedexEntryId) {
|
} else if (data.pokedexEntryId) {
|
||||||
// Try to find existing record for this user and pokemon
|
// Try to find existing record for this user, pokemon, and pokedex
|
||||||
const existing = await this.findByUserAndPokemon(this.userId, data.pokedexEntryId);
|
const existing = await this.findByUserAndPokemon(this.userId, data.pokedexEntryId, this.pokedexId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return this.update(existing._id, data);
|
return this.update(existing._id, data);
|
||||||
} else {
|
} else {
|
||||||
@@ -139,7 +148,7 @@ class CatchRecordRepository {
|
|||||||
return this.create(data);
|
return this.create(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByUserAndPokemon(userId: string, pokedexEntryId: string): Promise<CatchRecord | null> {
|
async findByUserAndPokemon(userId: string, pokedexEntryId: string, pokedexId: string): Promise<CatchRecord | null> {
|
||||||
const numericId = Number(pokedexEntryId);
|
const numericId = Number(pokedexEntryId);
|
||||||
if (isNaN(numericId)) {
|
if (isNaN(numericId)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -149,6 +158,7 @@ class CatchRecordRepository {
|
|||||||
.select('*')
|
.select('*')
|
||||||
.eq('userId', userId)
|
.eq('userId', userId)
|
||||||
.eq('pokedexEntryId', numericId)
|
.eq('pokedexEntryId', numericId)
|
||||||
|
.eq('pokedexId', pokedexId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error || !data) return null;
|
if (error || !data) return null;
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { type PokedexEntry, type PokedexEntryDB } from '$lib/models/PokedexEntry
|
|||||||
import { type CatchRecord, type CatchRecordDB } from '$lib/models/CatchRecord';
|
import { type CatchRecord, type CatchRecordDB } from '$lib/models/CatchRecord';
|
||||||
import { type CombinedData } from '$lib/models/CombinedData';
|
import { type CombinedData } from '$lib/models/CombinedData';
|
||||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
import { getRegionalDexColumnName } from '$lib/utils/regionalDexMapping';
|
||||||
|
|
||||||
class CombinedDataRepository {
|
class CombinedDataRepository {
|
||||||
constructor(
|
constructor(
|
||||||
private supabase: SupabaseClient,
|
private supabase: SupabaseClient,
|
||||||
private userId: string
|
private userId: string | null,
|
||||||
|
private pokedexId: string | null
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Transform Supabase data to match frontend expectations (minimal transformation)
|
// Transform Supabase data to match frontend expectations (minimal transformation)
|
||||||
@@ -31,7 +33,23 @@ class CombinedDataRepository {
|
|||||||
box: entry.boxPlacementBox || 0,
|
box: entry.boxPlacementBox || 0,
|
||||||
row: entry.boxPlacementRow || 0,
|
row: entry.boxPlacementRow || 0,
|
||||||
column: entry.boxPlacementColumn || 0
|
column: entry.boxPlacementColumn || 0
|
||||||
}
|
},
|
||||||
|
kantoDexNumber: entry.kanto_dex_number ?? undefined,
|
||||||
|
johtoDexNumber: entry.johto_dex_number ?? undefined,
|
||||||
|
hoennDexNumber: entry.hoenn_dex_number ?? undefined,
|
||||||
|
sinnohDexNumber: entry.sinnoh_dex_number ?? undefined,
|
||||||
|
unovaBwDexNumber: entry.unova_bw_dex_number ?? undefined,
|
||||||
|
unovaB2w2DexNumber: entry.unova_b2w2_dex_number ?? undefined,
|
||||||
|
kalosCentralDexNumber: entry.kalos_central_dex_number ?? undefined,
|
||||||
|
kalosCoastalDexNumber: entry.kalos_coastal_dex_number ?? undefined,
|
||||||
|
kalosMountainDexNumber: entry.kalos_mountain_dex_number ?? undefined,
|
||||||
|
alolaSmDexNumber: entry.alola_sm_dex_number ?? undefined,
|
||||||
|
alolaUsumDexNumber: entry.alola_usum_dex_number ?? undefined,
|
||||||
|
galarDexNumber: entry.galar_dex_number ?? undefined,
|
||||||
|
galarIsleOfArmorDexNumber: entry.galar_isle_of_armor_dex_number ?? undefined,
|
||||||
|
galarCrownTundraDexNumber: entry.galar_crown_tundra_dex_number ?? undefined,
|
||||||
|
hisuiDexNumber: entry.hisui_dex_number ?? undefined,
|
||||||
|
paldeaDexNumber: entry.paldea_dex_number ?? undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +58,7 @@ class CombinedDataRepository {
|
|||||||
_id: record.id,
|
_id: record.id,
|
||||||
userId: record.userId,
|
userId: record.userId,
|
||||||
pokedexEntryId: record.pokedexEntryId.toString(),
|
pokedexEntryId: record.pokedexEntryId.toString(),
|
||||||
|
pokedexId: record.pokedexId,
|
||||||
haveToEvolve: record.haveToEvolve,
|
haveToEvolve: record.haveToEvolve,
|
||||||
caught: record.caught,
|
caught: record.caught,
|
||||||
inHome: record.inHome,
|
inHome: record.inHome,
|
||||||
@@ -69,17 +88,31 @@ class CombinedDataRepository {
|
|||||||
query = query.contains('gamesToCatchIn', [game]);
|
query = query.contains('gamesToCatchIn', [game]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order by box placement
|
// Determine ordering strategy based on game filter
|
||||||
if (enableForms) {
|
if (game) {
|
||||||
query = query
|
const regionalColumn = getRegionalDexColumnName(game);
|
||||||
.order('boxPlacementFormsBox', { ascending: true })
|
if (regionalColumn) {
|
||||||
.order('boxPlacementFormsRow', { ascending: true })
|
// Order by regional dex with national dex fallback for NULLs
|
||||||
.order('boxPlacementFormsColumn', { ascending: true });
|
query = query
|
||||||
|
.order(regionalColumn, { ascending: true, nullsFirst: false })
|
||||||
|
.order('pokedexNumber', { ascending: true });
|
||||||
|
} else {
|
||||||
|
// Game specified but has no regional dex mapping
|
||||||
|
query = query.order('pokedexNumber', { ascending: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
query = query
|
// No game filter - use pre-calculated box placement (national dex order)
|
||||||
.order('boxPlacementBox', { ascending: true })
|
if (enableForms) {
|
||||||
.order('boxPlacementRow', { ascending: true })
|
query = query
|
||||||
.order('boxPlacementColumn', { ascending: true });
|
.order('boxPlacementFormsBox', { ascending: true })
|
||||||
|
.order('boxPlacementFormsRow', { ascending: true })
|
||||||
|
.order('boxPlacementFormsColumn', { ascending: true });
|
||||||
|
} else {
|
||||||
|
query = query
|
||||||
|
.order('boxPlacementBox', { ascending: true })
|
||||||
|
.order('boxPlacementRow', { ascending: true })
|
||||||
|
.order('boxPlacementColumn', { ascending: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: entries, error: entriesError } = await query;
|
const { data: entries, error: entriesError } = await query;
|
||||||
@@ -95,12 +128,13 @@ class CombinedDataRepository {
|
|||||||
|
|
||||||
// Get catch records for all entries
|
// Get catch records for all entries
|
||||||
let catchRecords: CatchRecordDB[] = [];
|
let catchRecords: CatchRecordDB[] = [];
|
||||||
if (userId) {
|
if (userId && this.pokedexId) {
|
||||||
const entryIds = entries.map((entry) => entry.id);
|
const entryIds = entries.map((entry) => entry.id);
|
||||||
const { data: records, error: recordsError } = await this.supabase
|
const { data: records, error: recordsError } = await this.supabase
|
||||||
.from('catch_records')
|
.from('catch_records')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('userId', userId)
|
.eq('userId', userId)
|
||||||
|
.eq('pokedexId', this.pokedexId)
|
||||||
.in('pokedexEntryId', entryIds);
|
.in('pokedexEntryId', entryIds);
|
||||||
|
|
||||||
if (!recordsError && records) {
|
if (!recordsError && records) {
|
||||||
@@ -109,7 +143,7 @@ class CombinedDataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Combine the data exactly like master branch
|
// Combine the data exactly like master branch
|
||||||
return entries.map((entry) => {
|
const combinedData = entries.map((entry) => {
|
||||||
const userCatchRecord =
|
const userCatchRecord =
|
||||||
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
|
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
|
||||||
|
|
||||||
@@ -123,6 +157,12 @@ class CombinedDataRepository {
|
|||||||
catchRecord: transformedCatchRecord
|
catchRecord: transformedCatchRecord
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recalculate box placement if using regional dex ordering
|
||||||
|
if (game && getRegionalDexColumnName(game)) {
|
||||||
|
return this.recalculateBoxPlacement(combinedData, enableForms);
|
||||||
|
}
|
||||||
|
return combinedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findCombinedData(
|
async findCombinedData(
|
||||||
@@ -151,17 +191,31 @@ class CombinedDataRepository {
|
|||||||
query = query.contains('gamesToCatchIn', [game]);
|
query = query.contains('gamesToCatchIn', [game]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order by box placement
|
// Determine ordering strategy based on game filter
|
||||||
if (enableForms) {
|
if (game) {
|
||||||
query = query
|
const regionalColumn = getRegionalDexColumnName(game);
|
||||||
.order('boxPlacementFormsBox', { ascending: true })
|
if (regionalColumn) {
|
||||||
.order('boxPlacementFormsRow', { ascending: true })
|
// Order by regional dex with national dex fallback for NULLs
|
||||||
.order('boxPlacementFormsColumn', { ascending: true });
|
query = query
|
||||||
|
.order(regionalColumn, { ascending: true, nullsFirst: false })
|
||||||
|
.order('pokedexNumber', { ascending: true });
|
||||||
|
} else {
|
||||||
|
// Game specified but has no regional dex mapping
|
||||||
|
query = query.order('pokedexNumber', { ascending: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
query = query
|
// No game filter - use pre-calculated box placement (national dex order)
|
||||||
.order('boxPlacementBox', { ascending: true })
|
if (enableForms) {
|
||||||
.order('boxPlacementRow', { ascending: true })
|
query = query
|
||||||
.order('boxPlacementColumn', { ascending: true });
|
.order('boxPlacementFormsBox', { ascending: true })
|
||||||
|
.order('boxPlacementFormsRow', { ascending: true })
|
||||||
|
.order('boxPlacementFormsColumn', { ascending: true });
|
||||||
|
} else {
|
||||||
|
query = query
|
||||||
|
.order('boxPlacementBox', { ascending: true })
|
||||||
|
.order('boxPlacementRow', { ascending: true })
|
||||||
|
.order('boxPlacementColumn', { ascending: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: entries, error: entriesError } = await query;
|
const { data: entries, error: entriesError } = await query;
|
||||||
@@ -177,12 +231,13 @@ class CombinedDataRepository {
|
|||||||
|
|
||||||
// Get catch records for these entries
|
// Get catch records for these entries
|
||||||
let catchRecords: CatchRecordDB[] = [];
|
let catchRecords: CatchRecordDB[] = [];
|
||||||
if (userId) {
|
if (userId && this.pokedexId) {
|
||||||
const entryIds = entries.map((entry) => entry.id);
|
const entryIds = entries.map((entry) => entry.id);
|
||||||
const { data: records, error: recordsError } = await this.supabase
|
const { data: records, error: recordsError } = await this.supabase
|
||||||
.from('catch_records')
|
.from('catch_records')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('userId', userId)
|
.eq('userId', userId)
|
||||||
|
.eq('pokedexId', this.pokedexId)
|
||||||
.in('pokedexEntryId', entryIds);
|
.in('pokedexEntryId', entryIds);
|
||||||
|
|
||||||
if (!recordsError && records) {
|
if (!recordsError && records) {
|
||||||
@@ -191,7 +246,7 @@ class CombinedDataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Combine the data exactly like master branch
|
// Combine the data exactly like master branch
|
||||||
return entries.map((entry) => {
|
const combinedData = entries.map((entry) => {
|
||||||
const userCatchRecord =
|
const userCatchRecord =
|
||||||
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
|
catchRecords.find((record) => record.pokedexEntryId === entry.id) || null;
|
||||||
|
|
||||||
@@ -205,6 +260,12 @@ class CombinedDataRepository {
|
|||||||
catchRecord: transformedCatchRecord
|
catchRecord: transformedCatchRecord
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recalculate box placement if using regional dex ordering
|
||||||
|
if (game && getRegionalDexColumnName(game)) {
|
||||||
|
return this.recalculateBoxPlacement(combinedData, enableForms);
|
||||||
|
}
|
||||||
|
return combinedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async countCombinedData(
|
async countCombinedData(
|
||||||
@@ -236,6 +297,35 @@ class CombinedDataRepository {
|
|||||||
|
|
||||||
return count || 0;
|
return count || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates box placement based on the current order of data.
|
||||||
|
* Used when ordering by regional dex instead of pre-calculated national dex placement.
|
||||||
|
*/
|
||||||
|
private recalculateBoxPlacement(
|
||||||
|
data: CombinedData[],
|
||||||
|
useFormsPlacement: boolean
|
||||||
|
): CombinedData[] {
|
||||||
|
return data.map((item, index) => {
|
||||||
|
const placementIndex = index + 1; // 1-based indexing
|
||||||
|
const box = Math.ceil(placementIndex / 30);
|
||||||
|
const rowColumnIndex = (placementIndex - 1) % 30;
|
||||||
|
const row = Math.floor(rowColumnIndex / 6) + 1;
|
||||||
|
const column = (rowColumnIndex % 6) + 1;
|
||||||
|
|
||||||
|
const newPlacement = { box, row, column };
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
pokedexEntry: {
|
||||||
|
...item.pokedexEntry,
|
||||||
|
...(useFormsPlacement
|
||||||
|
? { boxPlacementForms: newPlacement }
|
||||||
|
: { boxPlacement: newPlacement })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CombinedDataRepository;
|
export default CombinedDataRepository;
|
||||||
|
|||||||
@@ -26,7 +26,23 @@ class PokedexEntryRepository {
|
|||||||
box: entry.boxPlacementBox || 0,
|
box: entry.boxPlacementBox || 0,
|
||||||
row: entry.boxPlacementRow || 0,
|
row: entry.boxPlacementRow || 0,
|
||||||
column: entry.boxPlacementColumn || 0
|
column: entry.boxPlacementColumn || 0
|
||||||
}
|
},
|
||||||
|
kantoDexNumber: entry.kanto_dex_number ?? undefined,
|
||||||
|
johtoDexNumber: entry.johto_dex_number ?? undefined,
|
||||||
|
hoennDexNumber: entry.hoenn_dex_number ?? undefined,
|
||||||
|
sinnohDexNumber: entry.sinnoh_dex_number ?? undefined,
|
||||||
|
unovaBwDexNumber: entry.unova_bw_dex_number ?? undefined,
|
||||||
|
unovaB2w2DexNumber: entry.unova_b2w2_dex_number ?? undefined,
|
||||||
|
kalosCentralDexNumber: entry.kalos_central_dex_number ?? undefined,
|
||||||
|
kalosCoastalDexNumber: entry.kalos_coastal_dex_number ?? undefined,
|
||||||
|
kalosMountainDexNumber: entry.kalos_mountain_dex_number ?? undefined,
|
||||||
|
alolaSmDexNumber: entry.alola_sm_dex_number ?? undefined,
|
||||||
|
alolaUsumDexNumber: entry.alola_usum_dex_number ?? undefined,
|
||||||
|
galarDexNumber: entry.galar_dex_number ?? undefined,
|
||||||
|
galarIsleOfArmorDexNumber: entry.galar_isle_of_armor_dex_number ?? undefined,
|
||||||
|
galarCrownTundraDexNumber: entry.galar_crown_tundra_dex_number ?? undefined,
|
||||||
|
hisuiDexNumber: entry.hisui_dex_number ?? undefined,
|
||||||
|
paldeaDexNumber: entry.paldea_dex_number ?? undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +90,7 @@ class PokedexEntryRepository {
|
|||||||
if (data.boxPlacement?.row !== undefined) dbData.boxPlacementRow = data.boxPlacement.row;
|
if (data.boxPlacement?.row !== undefined) dbData.boxPlacementRow = data.boxPlacement.row;
|
||||||
if (data.boxPlacement?.column !== undefined)
|
if (data.boxPlacement?.column !== undefined)
|
||||||
dbData.boxPlacementColumn = data.boxPlacement.column;
|
dbData.boxPlacementColumn = data.boxPlacement.column;
|
||||||
|
if (data.regionalDexNumbers !== undefined) dbData.regionalDexNumbers = data.regionalDexNumbers;
|
||||||
|
|
||||||
const { data: result, error } = await this.supabase
|
const { data: result, error } = await this.supabase
|
||||||
.from('pokedex_entries')
|
.from('pokedex_entries')
|
||||||
@@ -108,6 +125,7 @@ class PokedexEntryRepository {
|
|||||||
if (data.boxPlacement?.row !== undefined) dbData.boxPlacementRow = data.boxPlacement.row;
|
if (data.boxPlacement?.row !== undefined) dbData.boxPlacementRow = data.boxPlacement.row;
|
||||||
if (data.boxPlacement?.column !== undefined)
|
if (data.boxPlacement?.column !== undefined)
|
||||||
dbData.boxPlacementColumn = data.boxPlacement.column;
|
dbData.boxPlacementColumn = data.boxPlacement.column;
|
||||||
|
if (data.regionalDexNumbers !== undefined) dbData.regionalDexNumbers = data.regionalDexNumbers;
|
||||||
|
|
||||||
const { data: result, error } = await this.supabase
|
const { data: result, error } = await this.supabase
|
||||||
.from('pokedex_entries')
|
.from('pokedex_entries')
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import type { Pokedex, PokedexDB } from '$lib/models/Pokedex';
|
||||||
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
class PokedexRepository {
|
||||||
|
constructor(
|
||||||
|
private supabase: SupabaseClient,
|
||||||
|
private userId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private transform(db: PokedexDB): Pokedex {
|
||||||
|
return {
|
||||||
|
_id: db.id,
|
||||||
|
userId: db.userId,
|
||||||
|
name: db.name,
|
||||||
|
description: db.description || '',
|
||||||
|
isLivingDex: db.isLivingDex,
|
||||||
|
isShinyDex: db.isShinyDex,
|
||||||
|
isOriginDex: db.isOriginDex,
|
||||||
|
isFormDex: db.isFormDex,
|
||||||
|
gameScope: db.gameScope
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformToDatabase(data: Partial<Pokedex>): Partial<PokedexDB> {
|
||||||
|
const dbData: Partial<PokedexDB> = {};
|
||||||
|
if (data.name !== undefined) dbData.name = data.name;
|
||||||
|
if (data.description !== undefined) dbData.description = data.description;
|
||||||
|
if (data.isLivingDex !== undefined) dbData.isLivingDex = data.isLivingDex;
|
||||||
|
if (data.isShinyDex !== undefined) dbData.isShinyDex = data.isShinyDex;
|
||||||
|
if (data.isOriginDex !== undefined) dbData.isOriginDex = data.isOriginDex;
|
||||||
|
if (data.isFormDex !== undefined) dbData.isFormDex = data.isFormDex;
|
||||||
|
if (data.gameScope !== undefined) dbData.gameScope = data.gameScope;
|
||||||
|
return dbData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Pokedex | null> {
|
||||||
|
const { data, error } = await this.supabase
|
||||||
|
.from('pokedexes')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', id)
|
||||||
|
.eq('userId', this.userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !data) return null;
|
||||||
|
return this.transform(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Pokedex[]> {
|
||||||
|
const { data, error } = await this.supabase
|
||||||
|
.from('pokedexes')
|
||||||
|
.select('*')
|
||||||
|
.eq('userId', this.userId)
|
||||||
|
.order('createdAt', { ascending: true });
|
||||||
|
|
||||||
|
if (error || !data) return [];
|
||||||
|
return data.map((d) => this.transform(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<Pokedex>): Promise<Pokedex> {
|
||||||
|
const dbData = {
|
||||||
|
userId: this.userId,
|
||||||
|
...this.transformToDatabase(data)
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: result, error } = await this.supabase
|
||||||
|
.from('pokedexes')
|
||||||
|
.insert(dbData)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating pokedex:', error);
|
||||||
|
throw new Error(`Failed to create pokedex: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Failed to create pokedex: No result returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.transform(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<Pokedex>): Promise<Pokedex | null> {
|
||||||
|
const dbData = this.transformToDatabase(data);
|
||||||
|
|
||||||
|
const { data: result, error } = await this.supabase
|
||||||
|
.from('pokedexes')
|
||||||
|
.update(dbData)
|
||||||
|
.eq('id', id)
|
||||||
|
.eq('userId', this.userId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !result) return null;
|
||||||
|
return this.transform(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
const { error } = await this.supabase.from('pokedexes').delete().eq('id', id).eq('userId', this.userId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error deleting pokedex:', error);
|
||||||
|
throw new Error(`Failed to delete pokedex: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PokedexRepository;
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Maps game names to their corresponding regional Pokédex identifier.
|
||||||
|
* This mapping is used to determine which regional dex number to use when
|
||||||
|
* filtering and ordering Pokémon for a specific game.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type RegionalDexKey =
|
||||||
|
| 'kanto'
|
||||||
|
| 'johto'
|
||||||
|
| 'hoenn'
|
||||||
|
| 'sinnoh'
|
||||||
|
| 'unova_bw'
|
||||||
|
| 'unova_b2w2'
|
||||||
|
| 'kalos_central'
|
||||||
|
| 'kalos_coastal'
|
||||||
|
| 'kalos_mountain'
|
||||||
|
| 'alola_sm'
|
||||||
|
| 'alola_usum'
|
||||||
|
| 'galar'
|
||||||
|
| 'galar_isle_of_armor'
|
||||||
|
| 'galar_crown_tundra'
|
||||||
|
| 'hisui'
|
||||||
|
| 'paldea';
|
||||||
|
|
||||||
|
const gameToRegionalDexMap: Record<string, RegionalDexKey> = {
|
||||||
|
// Kanto region
|
||||||
|
'Red': 'kanto',
|
||||||
|
'Blue': 'kanto',
|
||||||
|
'Yellow': 'kanto',
|
||||||
|
'FireRed': 'kanto',
|
||||||
|
'LeafGreen': 'kanto',
|
||||||
|
'LG: Pikachu': 'kanto',
|
||||||
|
'LG: Eevee': 'kanto',
|
||||||
|
|
||||||
|
// Johto region
|
||||||
|
'Gold': 'johto',
|
||||||
|
'Silver': 'johto',
|
||||||
|
'Crystal': 'johto',
|
||||||
|
'HeartGold': 'johto',
|
||||||
|
'SoulSilver': 'johto',
|
||||||
|
|
||||||
|
// Hoenn region
|
||||||
|
'Ruby': 'hoenn',
|
||||||
|
'Sapphire': 'hoenn',
|
||||||
|
'Emerald': 'hoenn',
|
||||||
|
'OmegaRuby': 'hoenn',
|
||||||
|
'AlphaSapphire': 'hoenn',
|
||||||
|
|
||||||
|
// Sinnoh region
|
||||||
|
'Diamond': 'sinnoh',
|
||||||
|
'Pearl': 'sinnoh',
|
||||||
|
'Platinum': 'sinnoh',
|
||||||
|
'BrilliantDiamond': 'sinnoh',
|
||||||
|
'ShiningPearl': 'sinnoh',
|
||||||
|
|
||||||
|
// Unova region
|
||||||
|
'Black': 'unova_bw',
|
||||||
|
'White': 'unova_bw',
|
||||||
|
'Black2': 'unova_b2w2',
|
||||||
|
'White2': 'unova_b2w2',
|
||||||
|
|
||||||
|
// Kalos region - Note: All XY use all three sub-dexes
|
||||||
|
// We default to Central for simplicity
|
||||||
|
'X': 'kalos_central',
|
||||||
|
'Y': 'kalos_central',
|
||||||
|
|
||||||
|
// Alola region
|
||||||
|
'Sun': 'alola_sm',
|
||||||
|
'Moon': 'alola_sm',
|
||||||
|
'UltraSun': 'alola_usum',
|
||||||
|
'UltraMoon': 'alola_usum',
|
||||||
|
|
||||||
|
// Galar region
|
||||||
|
'Sword': 'galar',
|
||||||
|
'Shield': 'galar',
|
||||||
|
|
||||||
|
// Hisui region
|
||||||
|
'LegendsArceus': 'hisui',
|
||||||
|
|
||||||
|
// Paldea region
|
||||||
|
'Scarlet': 'paldea',
|
||||||
|
'Violet': 'paldea'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the regional dex key for a given game name.
|
||||||
|
* Returns undefined if the game doesn't have a regional dex mapping.
|
||||||
|
*/
|
||||||
|
export function getRegionalDexKey(gameName: string): RegionalDexKey | undefined {
|
||||||
|
return gameToRegionalDexMap[gameName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a game has a regional dex mapping.
|
||||||
|
*/
|
||||||
|
export function hasRegionalDex(gameName: string): boolean {
|
||||||
|
return gameName in gameToRegionalDexMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the database column name for a game's regional dex.
|
||||||
|
* Maps game name → database column name (e.g., 'Scarlet' → 'paldea_dex_number').
|
||||||
|
* Returns undefined if the game doesn't have a regional dex mapping.
|
||||||
|
*/
|
||||||
|
export function getRegionalDexColumnName(gameName: string): string | undefined {
|
||||||
|
const regionalKey = getRegionalDexKey(gameName);
|
||||||
|
if (!regionalKey) return undefined;
|
||||||
|
return `${regionalKey}_dex_number`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the frontend field name for a game's regional dex.
|
||||||
|
* Maps game name → PokedexEntry field name (e.g., 'Scarlet' → 'paldeaDexNumber').
|
||||||
|
* Returns undefined if the game doesn't have a regional dex mapping.
|
||||||
|
*/
|
||||||
|
export function getRegionalDexFieldName(gameName: string): string | undefined {
|
||||||
|
const regionalKey = getRegionalDexKey(gameName);
|
||||||
|
if (!regionalKey) return undefined;
|
||||||
|
|
||||||
|
// Map regional key to actual PokedexEntry field name (camelCase)
|
||||||
|
const fieldMap: Record<string, string> = {
|
||||||
|
'kanto': 'kantoDexNumber',
|
||||||
|
'johto': 'johtoDexNumber',
|
||||||
|
'hoenn': 'hoennDexNumber',
|
||||||
|
'sinnoh': 'sinnohDexNumber',
|
||||||
|
'unova_bw': 'unovaBwDexNumber',
|
||||||
|
'unova_b2w2': 'unovaB2w2DexNumber',
|
||||||
|
'kalos_central': 'kalosCentralDexNumber',
|
||||||
|
'kalos_coastal': 'kalosCoastalDexNumber',
|
||||||
|
'kalos_mountain': 'kalosMountainDexNumber',
|
||||||
|
'alola_sm': 'alolaSmDexNumber',
|
||||||
|
'alola_usum': 'alolaUsumDexNumber',
|
||||||
|
'galar': 'galarDexNumber',
|
||||||
|
'galar_isle_of_armor': 'galarIsleOfArmorDexNumber',
|
||||||
|
'galar_crown_tundra': 'galarCrownTundraDexNumber',
|
||||||
|
'hisui': 'hisuiDexNumber',
|
||||||
|
'paldea': 'paldeaDexNumber'
|
||||||
|
};
|
||||||
|
|
||||||
|
return fieldMap[regionalKey];
|
||||||
|
}
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
>
|
>
|
||||||
{#if localUser}
|
{#if localUser}
|
||||||
<li>
|
<li>
|
||||||
<a href="/mydex"> My Dex </a>
|
<a href="/my-pokedexes"> My Pokédexes </a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/profile"> Profile </a>
|
<a href="/profile"> Profile </a>
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
|
||||||
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
|
|
||||||
import { getOptionalUserId } from '$lib/utils/auth';
|
|
||||||
import type { RequestEvent } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const GET = async (event: RequestEvent) => {
|
|
||||||
const { url } = event;
|
|
||||||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
|
||||||
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
||||||
const enableForms = url.searchParams.get('enableForms') === 'true';
|
|
||||||
const region = url.searchParams.get('region') || '';
|
|
||||||
const game = url.searchParams.get('game') || '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get userId if authenticated, null if not
|
|
||||||
const userId = await getOptionalUserId(event);
|
|
||||||
|
|
||||||
// If user is authenticated, set their session on the Supabase client
|
|
||||||
if (userId) {
|
|
||||||
const { session } = await event.locals.safeGetSession();
|
|
||||||
if (session) {
|
|
||||||
await event.locals.supabase.auth.setSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const repo = new CombinedDataRepository(event.locals.supabase, userId || '');
|
|
||||||
const combinedData = await repo.findCombinedData(
|
|
||||||
userId || '',
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
enableForms,
|
|
||||||
region,
|
|
||||||
game
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return empty array instead of 404 for better UX
|
|
||||||
if (combinedData.length === 0) {
|
|
||||||
return json({ combinedData: [], totalPages: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalCount = await repo.countCombinedData(enableForms, region, game);
|
|
||||||
const totalPages = Math.ceil(totalCount / limit);
|
|
||||||
|
|
||||||
return json({ combinedData, totalPages });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
if (err && typeof err === 'object' && 'status' in err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return json({ error: 'Internal Server Error' }, { status: 500 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
|
||||||
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
|
|
||||||
import { getOptionalUserId } from '$lib/utils/auth';
|
|
||||||
import type { RequestEvent } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const GET = async (event: RequestEvent) => {
|
|
||||||
const { url } = event;
|
|
||||||
const enableForms = url.searchParams.get('enableForms') === 'true';
|
|
||||||
const region = url.searchParams.get('region') || '';
|
|
||||||
const game = url.searchParams.get('game') || '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get userId if authenticated, null if not
|
|
||||||
const userId = await getOptionalUserId(event);
|
|
||||||
|
|
||||||
// If user is authenticated, set their session on the Supabase client
|
|
||||||
if (userId) {
|
|
||||||
const { session } = await event.locals.safeGetSession();
|
|
||||||
if (session) {
|
|
||||||
await event.locals.supabase.auth.setSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const repo = new CombinedDataRepository(event.locals.supabase, userId || '');
|
|
||||||
const combinedData = await repo.findAllCombinedData(userId || '', enableForms, region, game);
|
|
||||||
|
|
||||||
// Return empty array instead of 404 for better UX
|
|
||||||
return json(combinedData);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
if (err && typeof err === 'object' && 'status' in err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return json({ error: 'Internal Server Error' }, { status: 500 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { Pokedex } from '$lib/models/Pokedex';
|
||||||
|
import PokedexRepository from '$lib/repositories/PokedexRepository';
|
||||||
|
import { requireAuth } from '$lib/utils/auth';
|
||||||
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
// GET: List all pokedexes for user
|
||||||
|
export const GET = async (event: RequestEvent) => {
|
||||||
|
try {
|
||||||
|
const userId = await requireAuth(event);
|
||||||
|
const repo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
const pokedexes = await repo.findAll();
|
||||||
|
return json(pokedexes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in GET /api/pokedexes:', err);
|
||||||
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// POST: Create new pokedex
|
||||||
|
export const POST = async (event: RequestEvent) => {
|
||||||
|
try {
|
||||||
|
console.log('POST /api/pokedexes - Starting request');
|
||||||
|
const userId = await requireAuth(event);
|
||||||
|
console.log('POST /api/pokedexes - User authenticated:', userId);
|
||||||
|
|
||||||
|
const data: Partial<Pokedex> = await event.request.json();
|
||||||
|
console.log('POST /api/pokedexes - Request data:', data);
|
||||||
|
|
||||||
|
data.userId = userId;
|
||||||
|
|
||||||
|
const repo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
const pokedex = await repo.create(data);
|
||||||
|
console.log('POST /api/pokedexes - Pokédex created:', pokedex._id);
|
||||||
|
|
||||||
|
return json(pokedex);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in POST /api/pokedexes:', err);
|
||||||
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { Pokedex } from '$lib/models/Pokedex';
|
||||||
|
import PokedexRepository from '$lib/repositories/PokedexRepository';
|
||||||
|
import { requireAuth } from '$lib/utils/auth';
|
||||||
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
// GET: Get single pokedex by ID
|
||||||
|
export const GET = async (event: RequestEvent) => {
|
||||||
|
try {
|
||||||
|
const userId = await requireAuth(event);
|
||||||
|
const { id } = event.params;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return json({ error: 'Pokedex ID is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session } = await event.locals.safeGetSession();
|
||||||
|
if (session) {
|
||||||
|
await event.locals.supabase.auth.setSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
const pokedex = await repo.findById(id);
|
||||||
|
|
||||||
|
if (!pokedex) {
|
||||||
|
// RLS ensures user can only access own pokédexes
|
||||||
|
// Return 404 whether it doesn't exist or user doesn't own it (don't leak info)
|
||||||
|
return json({ error: 'Pokedex not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(pokedex);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// PUT: Update pokedex
|
||||||
|
export const PUT = async (event: RequestEvent) => {
|
||||||
|
try {
|
||||||
|
const userId = await requireAuth(event);
|
||||||
|
const { id } = event.params;
|
||||||
|
const data: Partial<Pokedex> = await event.request.json();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return json({ error: 'Pokedex ID is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session } = await event.locals.safeGetSession();
|
||||||
|
if (session) {
|
||||||
|
await event.locals.supabase.auth.setSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
const pokedex = await repo.update(id, data);
|
||||||
|
|
||||||
|
if (!pokedex) {
|
||||||
|
return json({ error: 'Pokedex not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(pokedex);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DELETE: Delete pokedex
|
||||||
|
export const DELETE = async (event: RequestEvent) => {
|
||||||
|
try {
|
||||||
|
const userId = await requireAuth(event);
|
||||||
|
const { id } = event.params;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return json({ error: 'Pokedex ID is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session } = await event.locals.safeGetSession();
|
||||||
|
if (session) {
|
||||||
|
await event.locals.supabase.auth.setSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
|
||||||
|
// Verify pokedex exists and user owns it
|
||||||
|
const pokedex = await repo.findById(id);
|
||||||
|
if (!pokedex) {
|
||||||
|
return json({ error: 'Pokedex not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await repo.delete(id);
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
+53
-15
@@ -1,22 +1,35 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { type CatchRecord } from '$lib/models/CatchRecord';
|
import { type CatchRecord } from '$lib/models/CatchRecord';
|
||||||
import CatchRecordRepository from '$lib/repositories/CatchRecordRepository';
|
import CatchRecordRepository from '$lib/repositories/CatchRecordRepository';
|
||||||
|
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';
|
||||||
|
|
||||||
|
// GET: Get catch records for specific pokédex
|
||||||
export const GET = async (event: RequestEvent) => {
|
export const GET = async (event: RequestEvent) => {
|
||||||
try {
|
try {
|
||||||
const userId = await requireAuth(event);
|
const userId = await requireAuth(event);
|
||||||
|
const { id: pokedexId } = event.params;
|
||||||
|
|
||||||
|
if (!pokedexId) {
|
||||||
|
return json({ error: 'Pokedex ID is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// Get the user's session and set it on the Supabase client
|
|
||||||
const { session } = await event.locals.safeGetSession();
|
const { session } = await event.locals.safeGetSession();
|
||||||
if (session) {
|
if (session) {
|
||||||
await event.locals.supabase.auth.setSession(session);
|
await event.locals.supabase.auth.setSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repo = new CatchRecordRepository(event.locals.supabase, userId);
|
// Verify user owns this pokédex (RLS will also check, but explicit verification is better UX)
|
||||||
const catchData = await repo.findByUserId(userId);
|
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
// order by pokedexEntryId property, ascending
|
const pokedex = await pokedexRepo.findById(pokedexId);
|
||||||
|
|
||||||
|
if (!pokedex) {
|
||||||
|
return json({ error: 'Pokedex not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = new CatchRecordRepository(event.locals.supabase, userId, pokedexId);
|
||||||
|
const catchData = await repo.findAll();
|
||||||
const sortedData = catchData.sort(
|
const sortedData = catchData.sort(
|
||||||
(a, b) => Number(a.pokedexEntryId) - Number(b.pokedexEntryId)
|
(a, b) => Number(a.pokedexEntryId) - Number(b.pokedexEntryId)
|
||||||
);
|
);
|
||||||
@@ -30,24 +43,35 @@ export const GET = async (event: RequestEvent) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PUT: Upsert single catch record for specific pokédex
|
||||||
export const PUT = async (event: RequestEvent) => {
|
export const PUT = async (event: RequestEvent) => {
|
||||||
try {
|
try {
|
||||||
// Check if we can get a session first
|
|
||||||
const { session, user } = await event.locals.safeGetSession();
|
|
||||||
|
|
||||||
const userId = await requireAuth(event);
|
const userId = await requireAuth(event);
|
||||||
|
const { id: pokedexId } = event.params;
|
||||||
const data: Partial<CatchRecord> = await event.request.json();
|
const data: Partial<CatchRecord> = await event.request.json();
|
||||||
|
|
||||||
// Get the user's session and set it on the Supabase client
|
if (!pokedexId) {
|
||||||
|
return json({ error: 'Pokedex ID is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session } = await event.locals.safeGetSession();
|
||||||
if (session) {
|
if (session) {
|
||||||
await event.locals.supabase.auth.setSession(session);
|
await event.locals.supabase.auth.setSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the userId is set to the authenticated user
|
// Verify user owns this pokédex
|
||||||
data.userId = userId;
|
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
const pokedex = await pokedexRepo.findById(pokedexId);
|
||||||
|
|
||||||
const repo = new CatchRecordRepository(event.locals.supabase, userId);
|
if (!pokedex) {
|
||||||
|
return json({ error: 'Pokedex not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the data is scoped to this pokédex and user
|
||||||
|
data.userId = userId;
|
||||||
|
data.pokedexId = pokedexId;
|
||||||
|
|
||||||
|
const repo = new CatchRecordRepository(event.locals.supabase, userId, pokedexId);
|
||||||
const upsertedRecord = await repo.upsert(data);
|
const upsertedRecord = await repo.upsert(data);
|
||||||
return json(upsertedRecord);
|
return json(upsertedRecord);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -59,23 +83,37 @@ export const PUT = async (event: RequestEvent) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// POST: Bulk upsert catch records for specific pokédex
|
||||||
export const POST = async (event: RequestEvent) => {
|
export const POST = async (event: RequestEvent) => {
|
||||||
try {
|
try {
|
||||||
const userId = await requireAuth(event);
|
const userId = await requireAuth(event);
|
||||||
|
const { id: pokedexId } = event.params;
|
||||||
const records: Partial<CatchRecord>[] = await event.request.json();
|
const records: Partial<CatchRecord>[] = await event.request.json();
|
||||||
|
|
||||||
// Get the user's session and set it on the Supabase client
|
if (!pokedexId) {
|
||||||
|
return json({ error: 'Pokedex ID is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const { session } = await event.locals.safeGetSession();
|
const { session } = await event.locals.safeGetSession();
|
||||||
if (session) {
|
if (session) {
|
||||||
await event.locals.supabase.auth.setSession(session);
|
await event.locals.supabase.auth.setSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repo = new CatchRecordRepository(event.locals.supabase, userId);
|
// Verify user owns this pokédex
|
||||||
|
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
const pokedex = await pokedexRepo.findById(pokedexId);
|
||||||
|
|
||||||
|
if (!pokedex) {
|
||||||
|
return json({ error: 'Pokedex not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = new CatchRecordRepository(event.locals.supabase, userId, pokedexId);
|
||||||
|
|
||||||
const insertedRecords = [];
|
const insertedRecords = [];
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
// Ensure the userId is set to the authenticated user
|
// Ensure the data is scoped to this pokédex and user
|
||||||
record.userId = userId;
|
record.userId = userId;
|
||||||
|
record.pokedexId = pokedexId;
|
||||||
const upsertedRecord = await repo.upsert(record);
|
const upsertedRecord = await repo.upsert(record);
|
||||||
insertedRecords.push(upsertedRecord);
|
insertedRecords.push(upsertedRecord);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import CombinedDataRepository from '$lib/repositories/CombinedDataRepository';
|
||||||
|
import PokedexRepository from '$lib/repositories/PokedexRepository';
|
||||||
|
import { getOptionalUserId } from '$lib/utils/auth';
|
||||||
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
// GET: Get combined data (pokédex entries + catch records) for specific pokédex
|
||||||
|
export const GET = async (event: RequestEvent) => {
|
||||||
|
try {
|
||||||
|
const userId = await getOptionalUserId(event);
|
||||||
|
const { id: pokedexId } = event.params;
|
||||||
|
|
||||||
|
if (!pokedexId) {
|
||||||
|
return json({ error: 'Pokedex ID is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
const page = parseInt(url.searchParams.get('page') || '1');
|
||||||
|
const limit = parseInt(url.searchParams.get('limit') || '20');
|
||||||
|
const enableForms = url.searchParams.get('enableForms') === 'true';
|
||||||
|
const region = url.searchParams.get('region') || '';
|
||||||
|
const game = url.searchParams.get('game') || '';
|
||||||
|
|
||||||
|
// If authenticated, verify user owns this pokédex and get its gameScope
|
||||||
|
let pokedex;
|
||||||
|
if (userId) {
|
||||||
|
const pokedexRepo = new PokedexRepository(event.locals.supabase, userId);
|
||||||
|
pokedex = await pokedexRepo.findById(pokedexId);
|
||||||
|
|
||||||
|
if (!pokedex) {
|
||||||
|
// User is authenticated but doesn't own this pokédex (or it doesn't exist)
|
||||||
|
return json({ error: 'Pokedex not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Anonymous users cannot view pokédexes
|
||||||
|
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use pokédex's gameScope as default filter if no manual game filter is set
|
||||||
|
const effectiveGame = game || pokedex.gameScope || '';
|
||||||
|
|
||||||
|
const repo = new CombinedDataRepository(event.locals.supabase, userId, pokedexId);
|
||||||
|
|
||||||
|
// Get paginated combined data
|
||||||
|
const combinedData = await repo.findCombinedData(
|
||||||
|
userId!,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
enableForms,
|
||||||
|
region,
|
||||||
|
effectiveGame
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get total count for pagination
|
||||||
|
const totalCount = await repo.countCombinedData(enableForms, region, effectiveGame);
|
||||||
|
const totalPages = Math.ceil(totalCount / limit);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
combinedData,
|
||||||
|
totalPages,
|
||||||
|
currentPage: page,
|
||||||
|
totalCount
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { user } from '$lib/stores/user';
|
||||||
|
import type { Pokedex } from '$lib/models/Pokedex';
|
||||||
|
import PokedexCard from '$lib/components/pokedex/PokedexCard.svelte';
|
||||||
|
import PokedexForm from '$lib/components/pokedex/PokedexForm.svelte';
|
||||||
|
|
||||||
|
let pokedexes: Pokedex[] = [];
|
||||||
|
let showModal = false;
|
||||||
|
let editingPokedex: Pokedex | null = null;
|
||||||
|
let formData: Partial<Pokedex> = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
isLivingDex: false,
|
||||||
|
isShinyDex: false,
|
||||||
|
isOriginDex: false,
|
||||||
|
isFormDex: false,
|
||||||
|
gameScope: null
|
||||||
|
};
|
||||||
|
|
||||||
|
$: mode = editingPokedex ? ('edit' as const) : ('create' as const);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$user) {
|
||||||
|
goto('/signin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadPokedexes();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadPokedexes() {
|
||||||
|
const response = await fetch('/api/pokedexes', { credentials: 'include' });
|
||||||
|
if (response.ok) {
|
||||||
|
pokedexes = await response.json();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to load pokédexes:', response.status, await response.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
editingPokedex = null;
|
||||||
|
formData = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
isLivingDex: false,
|
||||||
|
isShinyDex: false,
|
||||||
|
isOriginDex: false,
|
||||||
|
isFormDex: false,
|
||||||
|
gameScope: null
|
||||||
|
};
|
||||||
|
showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(pokedex: Pokedex) {
|
||||||
|
editingPokedex = pokedex;
|
||||||
|
formData = { ...pokedex };
|
||||||
|
showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal = false;
|
||||||
|
editingPokedex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
if (editingPokedex) {
|
||||||
|
// Update existing pokédex
|
||||||
|
const response = await fetch(`/api/pokedexes/${editingPokedex._id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Failed to update pokédex:', response.status, errorText);
|
||||||
|
alert(`Failed to update pokédex: ${errorText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new pokédex
|
||||||
|
const response = await fetch('/api/pokedexes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Failed to create pokédex:', response.status, errorText);
|
||||||
|
alert(`Failed to create pokédex: ${errorText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the first pokédex, redirect to it after creation
|
||||||
|
const createdPokedex = await response.json();
|
||||||
|
if (pokedexes.length === 0) {
|
||||||
|
goto(`/pokedex/${createdPokedex._id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
await loadPokedexes();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving pokédex:', error);
|
||||||
|
alert('An error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(pokedex: Pokedex) {
|
||||||
|
// TODO: Get catch record count and show in confirmation
|
||||||
|
const confirmed = confirm(
|
||||||
|
`Are you sure you want to delete "${pokedex.name}"? This will also delete all associated catch records. This action cannot be undone.`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/pokedexes/${pokedex._id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Failed to delete pokédex:', response.status, errorText);
|
||||||
|
alert(`Failed to delete pokédex: ${errorText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadPokedexes();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting pokédex:', error);
|
||||||
|
alert('An error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleView(pokedex: Pokedex) {
|
||||||
|
goto(`/pokedex/${pokedex._id}`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>My Pokédexes - Living Dex Tracker</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-3xl font-bold">My Pokédexes</h1>
|
||||||
|
<button class="btn btn-primary" on:click={openCreateModal}> Create New Pokédex </button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if pokedexes.length === 0}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">You haven't created any pokédexes yet!</h2>
|
||||||
|
<p class="mb-6">Get started by creating your first pokédex to track your collection.</p>
|
||||||
|
<button class="btn btn-primary btn-lg" on:click={openCreateModal}>
|
||||||
|
Create Your First Pokédex
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{#each pokedexes as pokedex (pokedex._id)}
|
||||||
|
<PokedexCard
|
||||||
|
{pokedex}
|
||||||
|
onEdit={() => openEditModal(pokedex)}
|
||||||
|
onDelete={() => handleDelete(pokedex)}
|
||||||
|
onView={() => handleView(pokedex)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
{#if showModal}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="font-bold text-lg mb-4">
|
||||||
|
{mode === 'create' ? 'Create New Pokédex' : 'Edit Pokédex'}
|
||||||
|
</h3>
|
||||||
|
<PokedexForm
|
||||||
|
bind:pokedex={formData}
|
||||||
|
{mode}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={closeModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop" on:click={closeModal}></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
import PokedexRepository from '$lib/repositories/PokedexRepository';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||||
|
const { safeGetSession, supabase } = locals;
|
||||||
|
const { session, user } = await safeGetSession();
|
||||||
|
|
||||||
|
// Require authentication
|
||||||
|
if (!session || !user) {
|
||||||
|
throw redirect(303, '/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
// Fetch pokédex to verify ownership (RLS will also block, but we want a proper 404)
|
||||||
|
const repo = new PokedexRepository(supabase, user.id);
|
||||||
|
const pokedex = await repo.findById(id);
|
||||||
|
|
||||||
|
if (!pokedex) {
|
||||||
|
// Either doesn't exist or user doesn't own it
|
||||||
|
throw error(404, 'Pokédex not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pokedex
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
$: ({ supabase, session } = data);
|
// Get pokédex from server load
|
||||||
|
$: pokedex = data.pokedex;
|
||||||
|
$: pokedexId = pokedex._id;
|
||||||
|
|
||||||
let combinedData = null as CombinedData[] | null;
|
let combinedData = null as CombinedData[] | null;
|
||||||
let currentPage = 1 as number;
|
let currentPage = 1 as number;
|
||||||
@@ -64,20 +66,24 @@
|
|||||||
if (setCombinedDataToNull) {
|
if (setCombinedDataToNull) {
|
||||||
combinedData = null;
|
combinedData = null;
|
||||||
}
|
}
|
||||||
let endpoint = viewAsBoxes
|
// Use new pokédex-scoped endpoint
|
||||||
? `/api/combined-data/all?enableForms=${showForms}®ion=${catchRegion}&game=${catchGame}`
|
let endpoint = `/api/pokedexes/${pokedexId}/combined-data?page=${currentPage}&limit=${viewAsBoxes ? 9999 : itemsPerPage}&enableForms=${showForms}®ion=${catchRegion}&game=${catchGame}`;
|
||||||
: `/api/combined-data?page=${currentPage}&limit=${itemsPerPage}&enableForms=${showForms}®ion=${catchRegion}&game=${catchGame}`;
|
|
||||||
const response = await fetch(endpoint);
|
const response = await fetch(endpoint);
|
||||||
const data = await response.json();
|
const fetchedData = await response.json();
|
||||||
if (data.error) {
|
if (fetchedData.error) {
|
||||||
failedToLoad = true;
|
failedToLoad = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
combinedData = viewAsBoxes ? data : data.combinedData;
|
combinedData = fetchedData.combinedData;
|
||||||
totalPages = data.totalPages || 0;
|
totalPages = fetchedData.totalPages || 0;
|
||||||
if (viewAsBoxes) {
|
if (viewAsBoxes) {
|
||||||
boxNumbers = [
|
boxNumbers = [
|
||||||
...new Set(combinedData.map(({ pokedexEntry }) => pokedexEntry[currentPlacement].box))
|
...new Set(
|
||||||
|
combinedData.map(({ pokedexEntry }) =>
|
||||||
|
showForms ? pokedexEntry.boxPlacementForms.box : pokedexEntry.boxPlacement.box
|
||||||
|
)
|
||||||
|
)
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +105,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/catch-records', requestOptions);
|
// Use new pokédex-scoped endpoint
|
||||||
|
const response = await fetch(`/api/pokedexes/${pokedexId}/catch-records`, requestOptions);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('Server response:', response.status, errorText);
|
console.error('Server response:', response.status, errorText);
|
||||||
@@ -121,13 +128,17 @@
|
|||||||
inHome: boolean | null = null
|
inHome: boolean | null = null
|
||||||
) {
|
) {
|
||||||
let catchRecordsToUpdate = combinedData
|
let catchRecordsToUpdate = combinedData
|
||||||
.filter(({ pokedexEntry }) => pokedexEntry[currentPlacement].box === boxNumber)
|
.filter(({ pokedexEntry }) =>
|
||||||
|
(showForms ? pokedexEntry.boxPlacementForms.box : pokedexEntry.boxPlacement.box) ===
|
||||||
|
boxNumber
|
||||||
|
)
|
||||||
.map(({ pokedexEntry, catchRecord }) => {
|
.map(({ pokedexEntry, catchRecord }) => {
|
||||||
// Create default record if null
|
// Create default record if null
|
||||||
const baseRecord = catchRecord || {
|
const baseRecord = catchRecord || {
|
||||||
_id: '',
|
_id: '',
|
||||||
userId: localUser?.id || '',
|
userId: localUser?.id || '',
|
||||||
pokedexEntryId: pokedexEntry._id,
|
pokedexEntryId: pokedexEntry._id,
|
||||||
|
pokedexId: pokedexId,
|
||||||
haveToEvolve: false,
|
haveToEvolve: false,
|
||||||
caught: false,
|
caught: false,
|
||||||
inHome: false,
|
inHome: false,
|
||||||
@@ -150,7 +161,8 @@
|
|||||||
}
|
}
|
||||||
return updatedRecord;
|
return updatedRecord;
|
||||||
});
|
});
|
||||||
await fetch('/api/catch-records', {
|
// Use new pokédex-scoped endpoint
|
||||||
|
await fetch(`/api/pokedexes/${pokedexId}/catch-records`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -198,6 +210,7 @@
|
|||||||
const newCatchRecords = pokedexEntries.map((entry: PokedexEntry) => ({
|
const newCatchRecords = pokedexEntries.map((entry: PokedexEntry) => ({
|
||||||
userId: localUser?.id,
|
userId: localUser?.id,
|
||||||
pokedexEntryId: entry._id,
|
pokedexEntryId: entry._id,
|
||||||
|
pokedexId: pokedexId,
|
||||||
haveToEvolve: false,
|
haveToEvolve: false,
|
||||||
caught: false,
|
caught: false,
|
||||||
inHome: false,
|
inHome: false,
|
||||||
@@ -221,7 +234,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/catch-records', requestOptions);
|
// Use new pokédex-scoped endpoint
|
||||||
|
const response = await fetch(`/api/pokedexes/${pokedexId}/catch-records`, requestOptions);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to create catch records');
|
throw new Error('Failed to create catch records');
|
||||||
}
|
}
|
||||||
@@ -253,7 +267,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Living Dex Tracker - My Dex</title>
|
<title>{pokedex.name} - Living Dex Tracker</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if !localUser}
|
{#if !localUser}
|
||||||
@@ -272,6 +286,7 @@
|
|||||||
{#if viewAsBoxes}
|
{#if viewAsBoxes}
|
||||||
<PokedexViewBoxes
|
<PokedexViewBoxes
|
||||||
bind:showShiny
|
bind:showShiny
|
||||||
|
bind:showForms
|
||||||
bind:combinedData
|
bind:combinedData
|
||||||
bind:boxNumbers
|
bind:boxNumbers
|
||||||
bind:currentPlacement
|
bind:currentPlacement
|
||||||
@@ -293,6 +308,7 @@
|
|||||||
bind:totalRecordsCreated
|
bind:totalRecordsCreated
|
||||||
bind:failedToLoad
|
bind:failedToLoad
|
||||||
userId={localUser?.id}
|
userId={localUser?.id}
|
||||||
|
pokedexId={pokedexId}
|
||||||
{updateACatch}
|
{updateACatch}
|
||||||
{createCatchRecords}
|
{createCatchRecords}
|
||||||
/>
|
/>
|
||||||
@@ -301,6 +317,7 @@
|
|||||||
<div class="drawer-side lg:relative">
|
<div class="drawer-side lg:relative">
|
||||||
<label for="my-drawer" class="drawer-overlay lg:hidden"></label>
|
<label for="my-drawer" class="drawer-overlay lg:hidden"></label>
|
||||||
<PokedexSidebar
|
<PokedexSidebar
|
||||||
|
{pokedex}
|
||||||
bind:viewAsBoxes
|
bind:viewAsBoxes
|
||||||
bind:currentPage
|
bind:currentPage
|
||||||
bind:itemsPerPage
|
bind:itemsPerPage
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
if (session) {
|
if (session) {
|
||||||
localUser = session.user;
|
localUser = session.user;
|
||||||
user.set(localUser);
|
user.set(localUser);
|
||||||
await goto('/mydex', { replace: true });
|
await goto('/my-pokedexes');
|
||||||
} else {
|
} else {
|
||||||
localUser = null;
|
localUser = null;
|
||||||
user.set(localUser);
|
user.set(localUser);
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- Create pokedexes table
|
||||||
|
CREATE TABLE pokedexes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
"userId" UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Type flags (combinable)
|
||||||
|
"isLivingDex" BOOLEAN DEFAULT FALSE,
|
||||||
|
"isShinyDex" BOOLEAN DEFAULT FALSE,
|
||||||
|
"isOriginDex" BOOLEAN DEFAULT FALSE,
|
||||||
|
"isFormDex" BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Scope (NULL = all games, or specific game)
|
||||||
|
"gameScope" TEXT,
|
||||||
|
|
||||||
|
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
"updatedAt" TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- User can't have duplicate pokédex names
|
||||||
|
UNIQUE("userId", name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index for performance
|
||||||
|
CREATE INDEX idx_pokedexes_user_id ON pokedexes("userId");
|
||||||
|
|
||||||
|
-- Trigger to automatically update updated_at timestamp
|
||||||
|
CREATE TRIGGER update_pokedexes_updated_at
|
||||||
|
BEFORE UPDATE ON pokedexes
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE pokedexes ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- RLS POLICIES - CRITICAL: Users can ONLY access their own pokédexes
|
||||||
|
|
||||||
|
-- Users can ONLY view their own pokédexes
|
||||||
|
CREATE POLICY "Users can view own pokedexes" ON pokedexes
|
||||||
|
FOR SELECT USING (auth.uid() = "userId");
|
||||||
|
|
||||||
|
-- Users can ONLY create pokédexes for themselves
|
||||||
|
CREATE POLICY "Users can create own pokedexes" ON pokedexes
|
||||||
|
FOR INSERT WITH CHECK (auth.uid() = "userId");
|
||||||
|
|
||||||
|
-- Users can ONLY update their own pokédexes
|
||||||
|
CREATE POLICY "Users can update own pokedexes" ON pokedexes
|
||||||
|
FOR UPDATE USING (auth.uid() = "userId");
|
||||||
|
|
||||||
|
-- Users can ONLY delete their own pokédexes
|
||||||
|
CREATE POLICY "Users can delete own pokedexes" ON pokedexes
|
||||||
|
FOR DELETE USING (auth.uid() = "userId");
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
-- Truncate existing catch records (no real users, as confirmed)
|
||||||
|
TRUNCATE TABLE catch_records;
|
||||||
|
|
||||||
|
-- Add pokedexId column
|
||||||
|
ALTER TABLE catch_records
|
||||||
|
ADD COLUMN "pokedexId" UUID REFERENCES pokedexes(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Make pokedexId NOT NULL
|
||||||
|
ALTER TABLE catch_records
|
||||||
|
ALTER COLUMN "pokedexId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- Drop old unique constraint (userId, pokedexEntryId)
|
||||||
|
ALTER TABLE catch_records
|
||||||
|
DROP CONSTRAINT IF EXISTS catch_records_userId_pokedexEntryId_key;
|
||||||
|
|
||||||
|
-- Add new unique constraint (userId, pokedexEntryId, pokedexId)
|
||||||
|
ALTER TABLE catch_records
|
||||||
|
ADD CONSTRAINT catch_records_user_pokemon_pokedex_unique
|
||||||
|
UNIQUE("userId", "pokedexEntryId", "pokedexId");
|
||||||
|
|
||||||
|
-- Add index for performance
|
||||||
|
CREATE INDEX idx_catch_records_pokedex_id ON catch_records("pokedexId");
|
||||||
|
|
||||||
|
-- RLS POLICIES - CRITICAL: Users can ONLY access catch records for their own pokédexes
|
||||||
|
|
||||||
|
-- Drop existing RLS policies if they exist
|
||||||
|
DROP POLICY IF EXISTS "Users can view own catch records" ON catch_records;
|
||||||
|
DROP POLICY IF EXISTS "Users can insert own catch records" ON catch_records;
|
||||||
|
DROP POLICY IF EXISTS "Users can update own catch records" ON catch_records;
|
||||||
|
DROP POLICY IF EXISTS "Users can delete own catch records" ON catch_records;
|
||||||
|
|
||||||
|
-- Users can ONLY view catch records for their own pokédexes
|
||||||
|
CREATE POLICY "Users can view own catch records" ON catch_records
|
||||||
|
FOR SELECT USING (
|
||||||
|
auth.uid() = "userId" AND
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM pokedexes
|
||||||
|
WHERE pokedexes.id = catch_records."pokedexId"
|
||||||
|
AND pokedexes."userId" = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Users can ONLY insert catch records for their own pokédexes
|
||||||
|
CREATE POLICY "Users can insert own catch records" ON catch_records
|
||||||
|
FOR INSERT WITH CHECK (
|
||||||
|
auth.uid() = "userId" AND
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM pokedexes
|
||||||
|
WHERE pokedexes.id = catch_records."pokedexId"
|
||||||
|
AND pokedexes."userId" = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Users can ONLY update catch records for their own pokédexes
|
||||||
|
CREATE POLICY "Users can update own catch records" ON catch_records
|
||||||
|
FOR UPDATE USING (
|
||||||
|
auth.uid() = "userId" AND
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM pokedexes
|
||||||
|
WHERE pokedexes.id = catch_records."pokedexId"
|
||||||
|
AND pokedexes."userId" = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Users can ONLY delete catch records for their own pokédexes
|
||||||
|
CREATE POLICY "Users can delete own catch records" ON catch_records
|
||||||
|
FOR DELETE USING (
|
||||||
|
auth.uid() = "userId" AND
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM pokedexes
|
||||||
|
WHERE pokedexes.id = catch_records."pokedexId"
|
||||||
|
AND pokedexes."userId" = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- Add regional dex number columns to pokedex_entries table
|
||||||
|
-- Each region gets its own integer column for better query performance
|
||||||
|
|
||||||
|
ALTER TABLE pokedex_entries
|
||||||
|
ADD COLUMN IF NOT EXISTS kanto_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS johto_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS hoenn_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS sinnoh_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS unova_bw_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS unova_b2w2_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS kalos_central_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS kalos_coastal_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS kalos_mountain_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS alola_sm_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS alola_usum_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS galar_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS galar_isle_of_armor_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS galar_crown_tundra_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS hisui_dex_number INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS paldea_dex_number INT;
|
||||||
|
|
||||||
|
-- Create indexes for better query performance when filtering/ordering by regional dex
|
||||||
|
-- Only indexing Kanto and Johto for now (other regions to be added incrementally)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pokedex_entries_kanto_dex ON pokedex_entries(kanto_dex_number) WHERE kanto_dex_number IS NOT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pokedex_entries_johto_dex ON pokedex_entries(johto_dex_number) WHERE johto_dex_number IS NOT NULL;
|
||||||
Reference in New Issue
Block a user