feat(*): Add support for catch records to My Dex page

This commit is contained in:
Josh Creek
2024-07-12 14:19:44 +01:00
parent 0e8af37309
commit e7f1930eff
6 changed files with 460 additions and 31 deletions
@@ -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>
+24
View File
@@ -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;
+7
View File
@@ -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;
+58
View File
@@ -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
View File
@@ -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>