mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-13 02:53:45 +00:00
feat(*): Add support for catch records to My Dex page
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
<script lang="ts">
|
||||
import type { CatchRecord } from '$lib/models/CatchRecord';
|
||||
import type { PokedexEntry } from '$lib/models/PokedexEntry';
|
||||
import PokemonSprite from '../PokemonSprite.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let pokedexEntry: PokedexEntry;
|
||||
export let catchRecord: CatchRecord;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function save() {
|
||||
dispatch('updateCatch', { pokedexEntry, catchRecord });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="container bg-red-500 text-white rounded-lg shadow-md p-6 flex flex-col md:flex-row gap-4"
|
||||
>
|
||||
<div class="dex-column pokedex-entry-container">
|
||||
<div class="flex mb-2">
|
||||
<div class="sprite-container flex justify-center items-center bg-white rounded-lg p-2">
|
||||
<!-- <PokemonSprite
|
||||
pokedexNumber={pokedexEntry.pokedexNumber.toString().padStart(3, '0')}
|
||||
form={pokedexEntry.form
|
||||
.replace(/[^a-zA-Z ]/g, '')
|
||||
.trim()
|
||||
.replace(/ /g, '-')}
|
||||
/> -->
|
||||
</div>
|
||||
<div class="pl-2">
|
||||
<h3 class="text-xl font-bold pt-1">{pokedexEntry.pokemon}</h3>
|
||||
<sub class="text-gray-200">#{pokedexEntry.pokedexNumber.toString().padStart(3, '0')}</sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white text-black rounded-lg p-4 mb-2">
|
||||
<p><strong>Form:</strong> {pokedexEntry.form ? pokedexEntry.form : '-'}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white text-black rounded-lg p-4">
|
||||
<p><strong>Box:</strong> {pokedexEntry.boxPlacement.box}</p>
|
||||
<p><strong>Row:</strong> {pokedexEntry.boxPlacement.row}</p>
|
||||
<p><strong>Column:</strong> {pokedexEntry.boxPlacement.column}</p>
|
||||
<p><strong>Can Gigantamax:</strong> {pokedexEntry.canGigantamax ? 'Yes' : 'No'}</p>
|
||||
<p><strong>Notes:</strong> {pokedexEntry.notes}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dex-column catch-record-container bg-white text-black rounded-lg p-4 mb-4 md:mb-0">
|
||||
<div class="flex items-center">
|
||||
<label class="block font-bold mr-2" for={`caughtInput-${catchRecord._id}`}>Caught:</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`caughtInput-${catchRecord._id}`}
|
||||
bind:checked={catchRecord.caught}
|
||||
class="form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="block font-bold mr-2" for={`haveToEvolveInput-${catchRecord._id}`}
|
||||
>Needs to evolve:</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`haveToEvolveInput-${catchRecord._id}`}
|
||||
bind:checked={catchRecord.haveToEvolve}
|
||||
class="form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="block font-bold mr-2" for={`inHomeInput-${catchRecord._id}`}>In home:</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`inHomeInput-${catchRecord._id}`}
|
||||
bind:checked={catchRecord.inHome}
|
||||
class="form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
{#if pokedexEntry.canGigantamax}
|
||||
<div class="flex items-center">
|
||||
<label class="block font-bold mr-2" for={`hasGigantamaxedInput-${catchRecord._id}`}
|
||||
>Has Gigantamaxed:</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`hasGigantamaxedInput-${catchRecord._id}`}
|
||||
bind:checked={catchRecord.hasGigantamaxed}
|
||||
class="form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<p>
|
||||
<label class="block font-bold mb-1" for={`personalNotesInput-${catchRecord._id}`}
|
||||
>Notes:</label
|
||||
>
|
||||
<textarea
|
||||
bind:value={catchRecord.personalNotes}
|
||||
id={`personalNotesInput-${catchRecord._id}`}
|
||||
class="form-textarea w-full p-2 border rounded"
|
||||
></textarea>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="dex-column additional-details-container">
|
||||
<div class="bg-white text-black rounded-lg p-4 mb-2">
|
||||
<p><strong>Region to Catch In:</strong> {pokedexEntry.regionToCatchIn}</p>
|
||||
<p><strong>Games to catch in:</strong></p>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each pokedexEntry.gamesToCatchIn as game}
|
||||
<li>{game}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if pokedexEntry.regionToEvolveIn}
|
||||
<p><strong>Region to Evolve In:</strong> {pokedexEntry.regionToEvolveIn}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="bg-white text-black rounded-lg p-4">
|
||||
<button on:click={save}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.dex-column {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.pokedex-entry-container h3 {
|
||||
color: #ffd700; /* Gold color */
|
||||
}
|
||||
|
||||
.sprite-container {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
border: 2px solid #000;
|
||||
border-radius: 0.25rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-checkbox:checked {
|
||||
background-color: #00bfff;
|
||||
background-size: 100% 100%;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 3rem;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import mongoose, { Schema, Document, Types } from 'mongoose';
|
||||
|
||||
export interface CatchRecord extends Document {
|
||||
userId: string;
|
||||
pokedexEntryId: Types.ObjectId;
|
||||
haveToEvolve: boolean;
|
||||
caught: boolean;
|
||||
inHome: boolean;
|
||||
hasGigantamaxed: boolean;
|
||||
personalNotes: string;
|
||||
}
|
||||
|
||||
const catchRecordSchema = new Schema<CatchRecord>({
|
||||
userId: String,
|
||||
pokedexEntryId: { type: Schema.Types.ObjectId, ref: 'PokedexEntry', required: true },
|
||||
haveToEvolve: { type: Boolean, default: false },
|
||||
caught: { type: Boolean, default: false },
|
||||
inHome: { type: Boolean, default: false },
|
||||
hasGigantamaxed: { type: Boolean, default: false },
|
||||
personalNotes: String
|
||||
});
|
||||
|
||||
const CatchRecordModel = mongoose.model<CatchRecord>('CatchRecord', catchRecordSchema);
|
||||
export default CatchRecordModel;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type PokedexEntry } from './PokedexEntry';
|
||||
import { type CatchRecord } from './CatchRecord';
|
||||
|
||||
export interface CombinedData {
|
||||
pokedexEntry: PokedexEntry;
|
||||
catchRecord: CatchRecord;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import CatchRecordModel, { type CatchRecord } from '$lib/models/CatchRecord';
|
||||
|
||||
class CatchRecordRepository {
|
||||
async findById(id: string): Promise<CatchRecord | null> {
|
||||
return CatchRecordModel.findById(id).exec();
|
||||
}
|
||||
|
||||
async findAll(): Promise<CatchRecord[]> {
|
||||
return CatchRecordModel.find().exec();
|
||||
}
|
||||
|
||||
async create(data: Partial<CatchRecord>): Promise<CatchRecord> {
|
||||
return CatchRecordModel.create(data);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<CatchRecord>): Promise<CatchRecord | null> {
|
||||
return CatchRecordModel.findByIdAndUpdate(id, data, { new: true }).exec();
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await CatchRecordModel.findByIdAndDelete(id).exec();
|
||||
}
|
||||
|
||||
async upsert(data: Partial<CatchRecord>): Promise<CatchRecord | null> {
|
||||
if (data._id) {
|
||||
// Update existing record
|
||||
return CatchRecordModel.findByIdAndUpdate(data._id, data, { new: true }).exec();
|
||||
} else {
|
||||
// Create new record
|
||||
return CatchRecordModel.create(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CatchRecordRepository;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { dbConnect, dbDisconnect } from '$lib/utils/db';
|
||||
import { type CatchRecord } from '$lib/models/CatchRecord';
|
||||
import CatchRecordRepository from '$lib/repositories/CatchRecordRepository';
|
||||
|
||||
export const GET = async () => {
|
||||
let catchData = null as CatchRecord[] | null;
|
||||
|
||||
try {
|
||||
await dbConnect();
|
||||
const repo = new CatchRecordRepository();
|
||||
catchData = await repo.findAll();
|
||||
// order by pokedexEntryId property, ascending
|
||||
catchData = catchData.sort((a, b) => a.pokedexEntryId - b.pokedexEntryId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
dbDisconnect();
|
||||
}
|
||||
|
||||
return json(catchData);
|
||||
};
|
||||
|
||||
export const PUT = async ({ request }) => {
|
||||
try {
|
||||
const data: Partial<CatchRecord> = await request.json();
|
||||
await dbConnect();
|
||||
const repo = new CatchRecordRepository();
|
||||
const upsertedRecord = await repo.upsert(data);
|
||||
return json(upsertedRecord);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw error(500, 'Internal Server Error');
|
||||
} finally {
|
||||
await dbDisconnect();
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = async ({ request }) => {
|
||||
try {
|
||||
const records: Partial<CatchRecord>[] = await request.json();
|
||||
await dbConnect();
|
||||
const repo = new CatchRecordRepository();
|
||||
|
||||
const insertedRecords = [];
|
||||
for (const record of records) {
|
||||
const upsertedRecord = await repo.upsert(record);
|
||||
insertedRecords.push(upsertedRecord);
|
||||
}
|
||||
|
||||
return json(insertedRecords);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw error(500, 'Internal Server Error');
|
||||
} finally {
|
||||
await dbDisconnect();
|
||||
}
|
||||
};
|
||||
+171
-31
@@ -1,49 +1,189 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { user } from '$lib/stores/user.js';
|
||||
import { type User } from '@supabase/auth-js';
|
||||
import mongoose, { Types } from 'mongoose';
|
||||
import { type PokedexEntry } from '$lib/models/PokedexEntry';
|
||||
import PokemonSprite from '$lib/components/PokemonSprite.svelte';
|
||||
import CatchRecordModel, { type CatchRecord } from '$lib/models/CatchRecord';
|
||||
import { type CombinedData } from '$lib/models/CombinedData';
|
||||
import PokedexEntryCatchRecord from '$lib/components/pokedex/PokedexEntryCatchRecord.svelte';
|
||||
|
||||
let pokemonData = null as PokedexEntry[] | null;
|
||||
let localUser: User | null;
|
||||
const unsubscribe = user.subscribe((value) => {
|
||||
localUser = value;
|
||||
});
|
||||
onDestroy(unsubscribe);
|
||||
|
||||
let pokedexEntries = null as PokedexEntry[] | null;
|
||||
let catchRecords = null as CatchRecord[] | null;
|
||||
let combinedData = null as CombinedData[] | null;
|
||||
|
||||
let creatingRecords = false;
|
||||
let totalRecordsCreated = 0;
|
||||
|
||||
async function fetchPokeDexEntries() {
|
||||
console.log('Fetching updated Pokedex entry data');
|
||||
const response = await fetch('/api/pokedexentries');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Pokémon data');
|
||||
}
|
||||
|
||||
pokemonData = await response.json();
|
||||
pokedexEntries = await response.json();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const cachedData = localStorage.getItem('pokeDexEntries');
|
||||
if (cachedData) {
|
||||
// Use cached data
|
||||
pokemonData = JSON.parse(cachedData);
|
||||
} else {
|
||||
await fetchPokeDexEntries();
|
||||
// Cache data in local storage
|
||||
localStorage.setItem('pokeDexEntries', JSON.stringify(pokemonData));
|
||||
async function fetchCatchRecords() {
|
||||
console.log('Fetching catch record data');
|
||||
const response = await fetch('/api/catch-records');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch catch record data');
|
||||
}
|
||||
|
||||
catchRecords = await response.json();
|
||||
|
||||
// If there are no catch records, make one for each pokedex entry
|
||||
if (catchRecords?.length === 0 && localUser?.id) {
|
||||
creatingRecords = true;
|
||||
const newCatchRecords = pokedexEntries.map((entry) => ({
|
||||
userId: localUser?.id,
|
||||
pokedexEntryId: entry._id,
|
||||
haveToEvolve: false,
|
||||
caught: false,
|
||||
inHome: false,
|
||||
hasGigantamaxed: false,
|
||||
personalNotes: ''
|
||||
}));
|
||||
|
||||
const batchSize = 500;
|
||||
for (let i = 0; i < newCatchRecords.length; i += batchSize) {
|
||||
const batch = newCatchRecords.slice(i, i + batchSize);
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(batch)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/catch-records', requestOptions);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create catch records');
|
||||
}
|
||||
|
||||
const createdRecords = await response.json();
|
||||
totalRecordsCreated += createdRecords.length;
|
||||
console.log(`Created ${createdRecords.length} catch records`);
|
||||
} catch (error) {
|
||||
console.error('Error creating catch records:', error);
|
||||
}
|
||||
}
|
||||
|
||||
await fetchCatchRecords();
|
||||
console.log('Created catch records for each pokedex entry');
|
||||
creatingRecords = false;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLastModifiedDate = async () => {
|
||||
const response = await fetch('/api/pokedex-metadata');
|
||||
const { lastModified } = await response.json();
|
||||
return new Date(lastModified);
|
||||
};
|
||||
|
||||
const cacheData = () => {
|
||||
localStorage.setItem('pokeDexEntries', JSON.stringify(pokedexEntries));
|
||||
localStorage.setItem('pokeDexLastUpdated', new Date().toISOString());
|
||||
};
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
const cachedData = localStorage.getItem('pokeDexEntries');
|
||||
const cachedDate = localStorage.getItem('pokeDexLastUpdated');
|
||||
|
||||
if (cachedData && cachedDate) {
|
||||
const lastModified = await fetchLastModifiedDate();
|
||||
const lastUpdated = new Date(cachedDate);
|
||||
|
||||
if (lastModified <= lastUpdated) {
|
||||
pokedexEntries = JSON.parse(cachedData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchPokeDexEntries();
|
||||
cacheData();
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await checkForUpdates()
|
||||
.then(async () => {
|
||||
await fetchCatchRecords();
|
||||
})
|
||||
.then(async () => {
|
||||
combinedData = [];
|
||||
|
||||
// Combine pokedex entries with their corresponding catch records
|
||||
pokedexEntries.forEach((entry) => {
|
||||
let catchRecord = catchRecords.find((record) => record.pokedexEntryId === entry._id);
|
||||
if (catchRecord) {
|
||||
combinedData.push({ pokedexEntry: entry, catchRecord });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function updateACatch(catchRecord: CatchRecord) {
|
||||
alert('here');
|
||||
// if (!localUser?.id) {
|
||||
// alert('User not signed in');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const requestOptions = {
|
||||
// method: 'PUT',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify(catchRecord)
|
||||
// };
|
||||
|
||||
// try {
|
||||
// const response = await fetch('/api/catch-records', requestOptions);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Failed to update catch record');
|
||||
// }
|
||||
|
||||
// const updatedRecord = await response.json();
|
||||
// console.log('Updated catch record:', updatedRecord);
|
||||
// } catch (error) {
|
||||
// console.error('Error updating catch record:', error);
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if pokemonData}
|
||||
{#each pokemonData as pokemon}
|
||||
<div class="container mx-auto">
|
||||
{#if combinedData}
|
||||
<div>
|
||||
<PokemonSprite
|
||||
pokedexNumber={pokemon.pokedexNumber.toString().padStart(3, '0')}
|
||||
form={pokemon.form
|
||||
.replace(/[^a-zA-Z ]/g, '')
|
||||
.trim()
|
||||
.replace(/ /g, '-')}
|
||||
/>
|
||||
<p>
|
||||
{pokemon.pokedexNumber.toString().padStart(3, '0')}
|
||||
{pokemon.pokemon}
|
||||
{pokemon.form}
|
||||
</p>
|
||||
<button>Toggle Forms</button>
|
||||
<button>Toggle Origins</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="loading loading-spinner loading-xl"></span>
|
||||
{/if}
|
||||
|
||||
<!-- <p>
|
||||
<button on:click={() => updateACatch(catchRecord)}>Save</button>
|
||||
</p> -->
|
||||
{#each combinedData as { pokedexEntry, catchRecord }}
|
||||
<div>
|
||||
<PokedexEntryCatchRecord
|
||||
{pokedexEntry}
|
||||
bind:catchRecord
|
||||
on:updateCatch={() => updateACatch(catchRecord)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="loading loading-spinner loading-xl"></span>
|
||||
<h1>Loading Pokédex</h1>
|
||||
<p>Please be patient, this can take some time on the first load.</p>
|
||||
{#if totalRecordsCreated > 0}
|
||||
<p>Processing {totalRecordsCreated}/{pokedexEntries?.length}</p>
|
||||
{:else if creatingRecords}
|
||||
<p>Processing...</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user