mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-13 19:13:43 +00:00
feat(#67): Add support for multiple pokedexes per user
This commit is contained in:
@@ -149,7 +149,7 @@
|
||||
>
|
||||
{#if localUser}
|
||||
<li>
|
||||
<a href="/mydex"> My Dex </a>
|
||||
<a href="/my-pokedexes"> My Pokédexes </a>
|
||||
</li>
|
||||
<li>
|
||||
<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 { type CatchRecord } from '$lib/models/CatchRecord';
|
||||
import CatchRecordRepository from '$lib/repositories/CatchRecordRepository';
|
||||
import PokedexRepository from '$lib/repositories/PokedexRepository';
|
||||
import { requireAuth } from '$lib/utils/auth';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
// GET: Get catch records for specific pokédex
|
||||
export const GET = async (event: RequestEvent) => {
|
||||
try {
|
||||
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();
|
||||
if (session) {
|
||||
await event.locals.supabase.auth.setSession(session);
|
||||
}
|
||||
|
||||
const repo = new CatchRecordRepository(event.locals.supabase, userId);
|
||||
const catchData = await repo.findByUserId(userId);
|
||||
// order by pokedexEntryId property, ascending
|
||||
// Verify user owns this pokédex (RLS will also check, but explicit verification is better UX)
|
||||
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 catchData = await repo.findAll();
|
||||
const sortedData = catchData.sort(
|
||||
(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) => {
|
||||
try {
|
||||
// Check if we can get a session first
|
||||
const { session, user } = await event.locals.safeGetSession();
|
||||
|
||||
const userId = await requireAuth(event);
|
||||
|
||||
const { id: pokedexId } = event.params;
|
||||
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) {
|
||||
await event.locals.supabase.auth.setSession(session);
|
||||
}
|
||||
|
||||
// Ensure the userId is set to the authenticated user
|
||||
data.userId = userId;
|
||||
// Verify user owns this pokédex
|
||||
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);
|
||||
return json(upsertedRecord);
|
||||
} 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) => {
|
||||
try {
|
||||
const userId = await requireAuth(event);
|
||||
const { id: pokedexId } = event.params;
|
||||
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();
|
||||
if (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 = [];
|
||||
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.pokedexId = pokedexId;
|
||||
const upsertedRecord = await repo.upsert(record);
|
||||
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;
|
||||
|
||||
$: ({ supabase, session } = data);
|
||||
// Get pokédex from server load
|
||||
$: pokedex = data.pokedex;
|
||||
$: pokedexId = pokedex._id;
|
||||
|
||||
let combinedData = null as CombinedData[] | null;
|
||||
let currentPage = 1 as number;
|
||||
@@ -64,20 +66,24 @@
|
||||
if (setCombinedDataToNull) {
|
||||
combinedData = null;
|
||||
}
|
||||
let endpoint = viewAsBoxes
|
||||
? `/api/combined-data/all?enableForms=${showForms}®ion=${catchRegion}&game=${catchGame}`
|
||||
: `/api/combined-data?page=${currentPage}&limit=${itemsPerPage}&enableForms=${showForms}®ion=${catchRegion}&game=${catchGame}`;
|
||||
// Use new pokédex-scoped endpoint
|
||||
let endpoint = `/api/pokedexes/${pokedexId}/combined-data?page=${currentPage}&limit=${viewAsBoxes ? 9999 : itemsPerPage}&enableForms=${showForms}®ion=${catchRegion}&game=${catchGame}`;
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
const fetchedData = await response.json();
|
||||
if (fetchedData.error) {
|
||||
failedToLoad = true;
|
||||
return;
|
||||
}
|
||||
combinedData = viewAsBoxes ? data : data.combinedData;
|
||||
totalPages = data.totalPages || 0;
|
||||
combinedData = fetchedData.combinedData;
|
||||
totalPages = fetchedData.totalPages || 0;
|
||||
if (viewAsBoxes) {
|
||||
boxNumbers = [
|
||||
...new Set(combinedData.map(({ pokedexEntry }) => pokedexEntry[currentPlacement].box))
|
||||
...new Set(
|
||||
combinedData.map(({ pokedexEntry }) =>
|
||||
showForms ? pokedexEntry.boxPlacementForms.box : pokedexEntry.boxPlacement.box
|
||||
)
|
||||
)
|
||||
].filter(Boolean);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +105,8 @@
|
||||
};
|
||||
|
||||
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) {
|
||||
const errorText = await response.text();
|
||||
console.error('Server response:', response.status, errorText);
|
||||
@@ -121,13 +128,17 @@
|
||||
inHome: boolean | null = null
|
||||
) {
|
||||
let catchRecordsToUpdate = combinedData
|
||||
.filter(({ pokedexEntry }) => pokedexEntry[currentPlacement].box === boxNumber)
|
||||
.filter(({ pokedexEntry }) =>
|
||||
(showForms ? pokedexEntry.boxPlacementForms.box : pokedexEntry.boxPlacement.box) ===
|
||||
boxNumber
|
||||
)
|
||||
.map(({ pokedexEntry, catchRecord }) => {
|
||||
// Create default record if null
|
||||
const baseRecord = catchRecord || {
|
||||
_id: '',
|
||||
userId: localUser?.id || '',
|
||||
pokedexEntryId: pokedexEntry._id,
|
||||
pokedexId: pokedexId,
|
||||
haveToEvolve: false,
|
||||
caught: false,
|
||||
inHome: false,
|
||||
@@ -150,7 +161,8 @@
|
||||
}
|
||||
return updatedRecord;
|
||||
});
|
||||
await fetch('/api/catch-records', {
|
||||
// Use new pokédex-scoped endpoint
|
||||
await fetch(`/api/pokedexes/${pokedexId}/catch-records`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -198,6 +210,7 @@
|
||||
const newCatchRecords = pokedexEntries.map((entry: PokedexEntry) => ({
|
||||
userId: localUser?.id,
|
||||
pokedexEntryId: entry._id,
|
||||
pokedexId: pokedexId,
|
||||
haveToEvolve: false,
|
||||
caught: false,
|
||||
inHome: false,
|
||||
@@ -221,7 +234,8 @@
|
||||
};
|
||||
|
||||
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) {
|
||||
throw new Error('Failed to create catch records');
|
||||
}
|
||||
@@ -253,7 +267,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Living Dex Tracker - My Dex</title>
|
||||
<title>{pokedex.name} - Living Dex Tracker</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if !localUser}
|
||||
@@ -272,6 +286,7 @@
|
||||
{#if viewAsBoxes}
|
||||
<PokedexViewBoxes
|
||||
bind:showShiny
|
||||
bind:showForms
|
||||
bind:combinedData
|
||||
bind:boxNumbers
|
||||
bind:currentPlacement
|
||||
@@ -293,6 +308,7 @@
|
||||
bind:totalRecordsCreated
|
||||
bind:failedToLoad
|
||||
userId={localUser?.id}
|
||||
pokedexId={pokedexId}
|
||||
{updateACatch}
|
||||
{createCatchRecords}
|
||||
/>
|
||||
@@ -301,6 +317,7 @@
|
||||
<div class="drawer-side lg:relative">
|
||||
<label for="my-drawer" class="drawer-overlay lg:hidden"></label>
|
||||
<PokedexSidebar
|
||||
{pokedex}
|
||||
bind:viewAsBoxes
|
||||
bind:currentPage
|
||||
bind:itemsPerPage
|
||||
@@ -24,7 +24,7 @@
|
||||
if (session) {
|
||||
localUser = session.user;
|
||||
user.set(localUser);
|
||||
await goto('/mydex', { replace: true });
|
||||
await goto('/my-pokedexes');
|
||||
} else {
|
||||
localUser = null;
|
||||
user.set(localUser);
|
||||
|
||||
Reference in New Issue
Block a user