mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-12 18:43:45 +00:00
chore(#67): Address pr comments
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Converts Kanto seed SQL migration to CSV format
|
||||
* Parses 20260104000001_seed_kanto.sql and generates:
|
||||
* - data/pokemon/gen1-kanto.csv
|
||||
* - data/pokemon/games.csv
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Read the SQL migration file
|
||||
const sqlPath = path.join(__dirname, '..', 'supabase', 'migrations', '20260104000001_seed_kanto.sql');
|
||||
const sqlContent = fs.readFileSync(sqlPath, 'utf-8');
|
||||
|
||||
// Extract games from region_game_mappings section
|
||||
const gameMatches = [...sqlContent.matchAll(/\('Kanto', '([^']+)'\)/g)];
|
||||
const games = gameMatches.map(m => m[1]);
|
||||
|
||||
console.log(`Found ${games.length} Kanto games:`, games);
|
||||
|
||||
// Extract Pokemon entries from INSERT statement
|
||||
// Match pattern: (number, 'name', form, boolean, 'region', ARRAY[games], ...)
|
||||
const valuePattern = /\((\d+),\s*'([^']+)',\s*(NULL|'[^']+'),\s*(true|false),\s*'[^']+',\s*ARRAY\[([^\]]+)\][^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,\s*(NULL|\d+)\)/g;
|
||||
|
||||
const pokemon = [];
|
||||
let match;
|
||||
while ((match = valuePattern.exec(sqlContent)) !== null) {
|
||||
const [_, pokedexNumber, name, formRaw, canGigantamax, gamesRaw, regionalNumberRaw] = match;
|
||||
|
||||
// Parse form (NULL or 'Female')
|
||||
const form = formRaw === 'NULL' ? '' : formRaw.replace(/'/g, '');
|
||||
|
||||
// Parse games array - extract game names from ARRAY['Red', 'Blue', ...]
|
||||
const gamesList = gamesRaw.match(/'([^']+)'/g).map(g => g.replace(/'/g, ''));
|
||||
const gamesStr = gamesList.join('|');
|
||||
|
||||
// Parse regional number
|
||||
const regionalNumber = regionalNumberRaw === 'NULL' ? '' : regionalNumberRaw;
|
||||
|
||||
pokemon.push({
|
||||
pokedexNumber,
|
||||
name,
|
||||
form,
|
||||
games: gamesStr,
|
||||
regionalNumber
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Extracted ${pokemon.length} Pokemon entries`);
|
||||
|
||||
// Generate gen1-kanto.csv
|
||||
const kantoCSV = [
|
||||
'pokedexNumber,name,form,games,regionalNumber',
|
||||
...pokemon.map(p => {
|
||||
const form = p.form.includes(',') ? `"${p.form}"` : p.form;
|
||||
const games = p.games.includes(',') ? `"${p.games}"` : p.games;
|
||||
return `${p.pokedexNumber},${p.name},${form},${games},${p.regionalNumber}`;
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
const kantoPath = path.join(__dirname, '..', 'data', 'pokemon', 'gen1-kanto.csv');
|
||||
fs.writeFileSync(kantoPath, kantoCSV);
|
||||
console.log(`✓ Created ${kantoPath}`);
|
||||
|
||||
// Generate games.csv
|
||||
const gamesCSV = [
|
||||
'id,displayName,region,generation',
|
||||
...games.map((game, idx) => {
|
||||
const id = game.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
const generation = game.startsWith('LG:') ? 7 : 1;
|
||||
return `${id},${game},Kanto,${generation}`;
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
const gamesPath = path.join(__dirname, '..', 'data', 'pokemon', 'games.csv');
|
||||
fs.writeFileSync(gamesPath, gamesCSV);
|
||||
console.log(`✓ Created ${gamesPath}`);
|
||||
|
||||
console.log('\n✅ Conversion complete!');
|
||||
console.log(` gen1-kanto.csv: ${pokemon.length} entries`);
|
||||
console.log(` games.csv: ${games.length} games`);
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { CatchRecord } from '$lib/models/CatchRecord';
|
||||
import type { CombinedData } from '$lib/models/CombinedData';
|
||||
import type { PokedexEntry } from '$lib/models/PokedexEntry';
|
||||
import { calculateBoxPlacement } from '$lib/utils/boxPlacement';
|
||||
import PokemonSprite from '$lib/components/PokemonSprite.svelte';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
|
||||
export let showShiny = false;
|
||||
export let showForms = true;
|
||||
export let combinedData: CombinedData[] | null;
|
||||
export let boxNumbers: number[] = [];
|
||||
export let creatingRecords = false;
|
||||
@@ -30,11 +29,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function cellBackgroundColourStyle(pokedexEntry: PokedexEntry, catchRecord: CatchRecord | null) {
|
||||
function cellBackgroundColourStyle(index: number, catchRecord: CatchRecord | null) {
|
||||
if (catchRecord?.caught || catchRecord?.haveToEvolve) {
|
||||
return '';
|
||||
} else {
|
||||
const placement = showForms ? pokedexEntry.boxPlacementForms : pokedexEntry.boxPlacement;
|
||||
const placement = calculateBoxPlacement(index);
|
||||
if (placement.column % 2 === 0) {
|
||||
return 'background-color: #ffffff';
|
||||
} else {
|
||||
@@ -68,16 +67,16 @@
|
||||
>Mark box as not 'in Home'</button
|
||||
>
|
||||
<div class="grid grid-cols-6">
|
||||
{#each combinedData as { pokedexEntry, catchRecord }}
|
||||
{@const placement = showForms
|
||||
? pokedexEntry.boxPlacementForms
|
||||
: pokedexEntry.boxPlacement}
|
||||
{#each combinedData as { pokedexEntry, catchRecord }, index}
|
||||
{@const placement = calculateBoxPlacement(index)}
|
||||
{#if placement.box === boxNumber}
|
||||
<button
|
||||
type="button"
|
||||
class="pokemon-box {cellBackgroundColourClass(catchRecord)} hover:scale-105 hover:shadow-lg hover:z-50 transition-all cursor-pointer relative"
|
||||
class="pokemon-box {cellBackgroundColourClass(
|
||||
catchRecord
|
||||
)} hover:scale-105 hover:shadow-lg hover:z-50 transition-all cursor-pointer relative"
|
||||
style="grid-column-start: {placement.column}; grid-row-start: {placement.row};
|
||||
{cellBackgroundColourStyle(pokedexEntry, catchRecord)}"
|
||||
{cellBackgroundColourStyle(index, catchRecord)}"
|
||||
on:click={() => onPokemonClick({ pokedexEntry, catchRecord })}
|
||||
aria-label="View details for {pokedexEntry.pokemon}"
|
||||
>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
export interface PokedexEntry {
|
||||
_id: string; // Maps to Supabase 'id' field for frontend compatibility
|
||||
pokedexNumber: number;
|
||||
boxPlacement: { box: number; row: number; column: number };
|
||||
boxPlacementForms: { box: number; row: number; column: number };
|
||||
pokemon: string;
|
||||
form: string;
|
||||
canGigantamax: boolean;
|
||||
|
||||
@@ -22,10 +22,7 @@ class CombinedDataRepository {
|
||||
gamesToCatchIn: entry.gamesToCatchIn || [],
|
||||
regionToEvolveIn: entry.regionToEvolveIn || '',
|
||||
evolutionInformation: entry.evolutionInformation || '',
|
||||
catchInformation: entry.catchInformation || [],
|
||||
// Box placement will be calculated dynamically after sorting
|
||||
boxPlacementForms: { box: 0, row: 0, column: 0 },
|
||||
boxPlacement: { box: 0, row: 0, column: 0 }
|
||||
catchInformation: entry.catchInformation || []
|
||||
// Note: Regional dex numbers are stored in separate regional_dex_numbers table
|
||||
};
|
||||
}
|
||||
@@ -113,8 +110,8 @@ class CombinedDataRepository {
|
||||
};
|
||||
});
|
||||
|
||||
// Always recalculate box placement based on current sort order
|
||||
return this.recalculateBoxPlacement(combinedData, enableForms);
|
||||
// Box placement is calculated dynamically on the frontend based on the current sort order.
|
||||
return combinedData;
|
||||
}
|
||||
|
||||
async findCombinedData(
|
||||
@@ -191,15 +188,11 @@ class CombinedDataRepository {
|
||||
};
|
||||
});
|
||||
|
||||
// Always recalculate box placement based on current sort order
|
||||
return this.recalculateBoxPlacement(combinedData, enableForms);
|
||||
// Box placement is calculated dynamically on the frontend based on the current sort order.
|
||||
return combinedData;
|
||||
}
|
||||
|
||||
async countCombinedData(
|
||||
enableForms: boolean,
|
||||
region: string,
|
||||
game: string
|
||||
): Promise<number> {
|
||||
async countCombinedData(enableForms: boolean, region: string, game: string): Promise<number> {
|
||||
let query = this.supabase.from('pokedex_entries').select('id', { count: 'exact', head: true });
|
||||
|
||||
// Apply same filters as in findCombinedData
|
||||
@@ -225,35 +218,6 @@ class CombinedDataRepository {
|
||||
|
||||
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;
|
||||
|
||||
@@ -16,26 +16,7 @@ class PokedexEntryRepository {
|
||||
gamesToCatchIn: entry.gamesToCatchIn || [],
|
||||
regionToEvolveIn: entry.regionToEvolveIn || '',
|
||||
evolutionInformation: entry.evolutionInformation || '',
|
||||
catchInformation: entry.catchInformation || [],
|
||||
// Box placement is calculated dynamically - not stored in database
|
||||
boxPlacementForms: { box: 0, row: 0, column: 0 },
|
||||
boxPlacement: { box: 0, row: 0, column: 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
|
||||
catchInformation: entry.catchInformation || []
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { PUBLIC_SUPABASE_URL } from '$env/static/public';
|
||||
import { SUPABASE_SERVICE_ROLE_KEY } from '$env/static/private';
|
||||
import dex from '$lib/helpers/pokedex.json';
|
||||
import RegionGameMappingJson from '$lib/helpers/region-game-mapping.json';
|
||||
|
||||
export const GET = async () => {
|
||||
// Only allow seeding in development
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return json({ message: 'Seeding not allowed in production' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Use service role key to bypass RLS for seeding operations
|
||||
const supabase = createClient(PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
|
||||
|
||||
try {
|
||||
// Seed Pokédex entries
|
||||
const { count: entriesCount, error: countError } = await supabase
|
||||
.from('pokedex_entries')
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
if (countError) {
|
||||
throw new Error(`Failed to check existing entries: ${countError.message}`);
|
||||
}
|
||||
|
||||
// Only seed if table is empty
|
||||
if (!entriesCount || entriesCount === 0) {
|
||||
console.log('Seeding Pokédex entries...');
|
||||
|
||||
// Transform data to match database schema
|
||||
const pokemonData = dex.map((pokemon: any) => ({
|
||||
pokedexNumber: pokemon.pokedexNumber,
|
||||
pokemon: pokemon.pokemon,
|
||||
form: pokemon.form || null,
|
||||
canGigantamax: pokemon.canGigantamax || false,
|
||||
regionToCatchIn: pokemon.regionToCatchIn || null,
|
||||
gamesToCatchIn: pokemon.gamesToCatchIn || null,
|
||||
regionToEvolveIn: pokemon.regionToEvolveIn || null,
|
||||
evolutionInformation: pokemon.evolutionInformation || null,
|
||||
catchInformation: pokemon.catchInformation || null,
|
||||
boxPlacementFormsBox: pokemon.boxPlacementForms?.box || null,
|
||||
boxPlacementFormsRow: pokemon.boxPlacementForms?.row || null,
|
||||
boxPlacementFormsColumn: pokemon.boxPlacementForms?.column || null,
|
||||
boxPlacementBox: pokemon.boxPlacement?.box || null,
|
||||
boxPlacementRow: pokemon.boxPlacement?.row || null,
|
||||
boxPlacementColumn: pokemon.boxPlacement?.column || null
|
||||
}));
|
||||
|
||||
const { error: insertError } = await supabase.from('pokedex_entries').insert(pokemonData);
|
||||
|
||||
if (insertError) {
|
||||
throw new Error(`Failed to seed Pokédex entries: ${insertError.message}`);
|
||||
}
|
||||
|
||||
console.log(`Seeded ${pokemonData.length} Pokédex entries`);
|
||||
} else {
|
||||
console.log('Pokédex entries already exist, skipping seeding');
|
||||
}
|
||||
|
||||
// Seed region-game mappings (check if table exists first)
|
||||
const { count: mappingsCount, error: mappingCountError } = await supabase
|
||||
.from('region_game_mappings')
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
if (!mappingCountError && (!mappingsCount || mappingsCount === 0)) {
|
||||
console.log('Seeding region-game mappings...');
|
||||
|
||||
const { error: mappingInsertError } = await supabase
|
||||
.from('region_game_mappings')
|
||||
.insert(RegionGameMappingJson);
|
||||
|
||||
if (mappingInsertError) {
|
||||
throw new Error(`Failed to seed region-game mappings: ${mappingInsertError.message}`);
|
||||
}
|
||||
|
||||
console.log(`Seeded ${RegionGameMappingJson.length} region-game mappings`);
|
||||
} else if (!mappingCountError) {
|
||||
console.log('Region-game mappings already exist, skipping seeding');
|
||||
}
|
||||
|
||||
return json({
|
||||
message: 'Seeding completed successfully',
|
||||
seeded: {
|
||||
pokemonEntries: !entriesCount || entriesCount === 0,
|
||||
regionGameMappings: !mappingCountError && (!mappingsCount || mappingsCount === 0)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Seeding failed:', error);
|
||||
return json(
|
||||
{
|
||||
message: 'Seeding failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -8,7 +8,7 @@ export const GET = async (event) => {
|
||||
} = event;
|
||||
const token_hash = url.searchParams.get('token_hash') as string;
|
||||
const type = url.searchParams.get('type') as EmailOtpType | null;
|
||||
const next = url.searchParams.get('next') ?? '/';
|
||||
const next = url.searchParams.get('next') ?? '/welcome';
|
||||
|
||||
if (token_hash && type) {
|
||||
const { error } = await supabase.auth.verifyOtp({ token_hash, type });
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
alert(`Failed to update pokédex: ${errorText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
closeModal();
|
||||
await loadPokedexes();
|
||||
return;
|
||||
} else {
|
||||
// Create new pokédex
|
||||
const response = await fetch('/api/pokedexes', {
|
||||
@@ -95,17 +99,18 @@
|
||||
alert(`Failed to create pokédex: ${errorText}`);
|
||||
return;
|
||||
}
|
||||
const createdPokedex = (await response.json()) as Pokedex;
|
||||
|
||||
// If this is the first pokédex, redirect to it after creation
|
||||
const createdPokedex = await response.json();
|
||||
if (pokedexes.length === 0) {
|
||||
// Refresh local state BEFORE deciding whether this is the user's first pokédex.
|
||||
closeModal();
|
||||
await loadPokedexes();
|
||||
|
||||
// If the user's total pokédex count is now 1, this newly created one is their first.
|
||||
if (pokedexes.length === 1) {
|
||||
goto(`/pokedex/${createdPokedex._id}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
closeModal();
|
||||
await loadPokedexes();
|
||||
} catch (error) {
|
||||
console.error('Error saving pokédex:', error);
|
||||
alert('An error occurred');
|
||||
@@ -184,12 +189,7 @@
|
||||
<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}
|
||||
/>
|
||||
<PokedexForm bind:pokedex={formData} {mode} onSubmit={handleSubmit} onCancel={closeModal} />
|
||||
</div>
|
||||
<div class="modal-backdrop" on:click={closeModal}></div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { user } from '$lib/stores/user.js';
|
||||
import { type User } from '@supabase/auth-js';
|
||||
import { type CombinedData } from '$lib/models/CombinedData';
|
||||
import { browser } from '$app/environment';
|
||||
import type { CatchRecord } from '$lib/models/CatchRecord';
|
||||
import type { PokedexEntry } from '$lib/models/PokedexEntry';
|
||||
import { calculateBoxNumbers, calculateBoxPlacement } from '$lib/utils/boxPlacement';
|
||||
import PokedexViewBoxes from '$lib/components/pokedex/PokedexViewBoxes.svelte';
|
||||
import PokedexModal from '$lib/components/pokedex/PokedexModal.svelte';
|
||||
import PokedexEntryCatchRecord from '$lib/components/pokedex/PokedexEntryCatchRecord.svelte';
|
||||
import type { Pokedex } from '$lib/models/Pokedex';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
// Get pokédex from server load
|
||||
$: pokedex = data.pokedex;
|
||||
$: pokedexId = pokedex._id;
|
||||
// Get pokédex from server load (guarded for transient undefined during navigation/HMR)
|
||||
let pokedex: Pokedex | undefined;
|
||||
let pokedexId = '';
|
||||
$: pokedex = data?.pokedex;
|
||||
$: pokedexId = pokedex?._id ?? '';
|
||||
|
||||
let combinedData = null as CombinedData[] | null;
|
||||
let currentPage = 1 as number;
|
||||
let itemsPerPage = 20 as number;
|
||||
// Box view requires the full dataset for correct box numbering/placement.
|
||||
// If/when a paginated list view is introduced, this can be lowered and paired with UI controls.
|
||||
let itemsPerPage = 9999 as number;
|
||||
let totalPages = 0 as number;
|
||||
let creatingRecords = false;
|
||||
let totalRecordsCreated = 0;
|
||||
@@ -29,8 +36,8 @@
|
||||
let selectedPokemon: CombinedData | null = null;
|
||||
|
||||
// Derive from pokedex config
|
||||
$: showOrigins = pokedex.isOriginDex;
|
||||
$: showShiny = pokedex.isShinyDex;
|
||||
$: showOrigins = !!pokedex?.isOriginDex;
|
||||
$: showShiny = !!pokedex?.isShinyDex;
|
||||
|
||||
const unsubscribe = user.subscribe((value) => {
|
||||
localUser = value;
|
||||
@@ -60,12 +67,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function getData(setCombinedDataToNull = true) {
|
||||
type GetDataOptions = {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
setCombinedDataToNull?: boolean;
|
||||
};
|
||||
|
||||
async function getData({
|
||||
page = currentPage,
|
||||
perPage = itemsPerPage,
|
||||
setCombinedDataToNull = true
|
||||
}: GetDataOptions = {}) {
|
||||
if (!pokedex || !pokedexId) return;
|
||||
if (setCombinedDataToNull) {
|
||||
combinedData = null;
|
||||
}
|
||||
// Use new pokédex-scoped endpoint - always fetch all data for box view
|
||||
let endpoint = `/api/pokedexes/${pokedexId}/combined-data?page=${currentPage}&limit=9999&enableForms=${pokedex.isFormDex}`;
|
||||
const effectivePage = Math.max(1, page);
|
||||
const effectivePerPage = Math.max(1, perPage);
|
||||
// Use new pokédex-scoped endpoint
|
||||
const endpoint = `/api/pokedexes/${pokedexId}/combined-data?page=${effectivePage}&limit=${effectivePerPage}&enableForms=${pokedex.isFormDex}`;
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
const fetchedData = await response.json();
|
||||
@@ -77,17 +97,12 @@
|
||||
totalPages = fetchedData.totalPages || 0;
|
||||
// Always extract box numbers for box view
|
||||
if (combinedData) {
|
||||
boxNumbers = [
|
||||
...new Set(
|
||||
combinedData.map(({ pokedexEntry }) =>
|
||||
pokedex.isFormDex ? pokedexEntry.boxPlacementForms.box : pokedexEntry.boxPlacement.box
|
||||
)
|
||||
)
|
||||
].filter(Boolean);
|
||||
boxNumbers = calculateBoxNumbers(combinedData.length);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateACatch(event: any) {
|
||||
if (!pokedexId) return;
|
||||
const { catchRecord } = event.detail;
|
||||
if (!localUser?.id) {
|
||||
alert('User not signed in');
|
||||
@@ -114,7 +129,7 @@
|
||||
}
|
||||
|
||||
// Refresh the data to reflect changes from server
|
||||
await getData(false);
|
||||
await getData({ page: currentPage, perPage: itemsPerPage, setCombinedDataToNull: false });
|
||||
} catch (error) {
|
||||
console.error('Error updating catch record:', error);
|
||||
}
|
||||
@@ -127,15 +142,13 @@
|
||||
inHome: boolean | null = null
|
||||
) {
|
||||
if (!combinedData) return;
|
||||
if (!pokedexId) return;
|
||||
|
||||
let catchRecordsToUpdate = combinedData
|
||||
.filter(({ pokedexEntry }) =>
|
||||
(pokedex.isFormDex ? pokedexEntry.boxPlacementForms.box : pokedexEntry.boxPlacement.box) ===
|
||||
boxNumber
|
||||
)
|
||||
const catchRecordsToUpdate: CatchRecord[] = combinedData
|
||||
.filter((_, index) => calculateBoxPlacement(index).box === boxNumber)
|
||||
.map(({ pokedexEntry, catchRecord }) => {
|
||||
// Create default record if null
|
||||
const baseRecord = catchRecord || {
|
||||
const baseRecord: CatchRecord = catchRecord ?? {
|
||||
_id: '',
|
||||
userId: localUser?.id || '',
|
||||
pokedexEntryId: pokedexEntry._id,
|
||||
@@ -147,7 +160,7 @@
|
||||
personalNotes: ''
|
||||
};
|
||||
|
||||
let updatedRecord = { ...baseRecord };
|
||||
let updatedRecord: CatchRecord = { ...baseRecord };
|
||||
if (inHome !== null) {
|
||||
updatedRecord = {
|
||||
...updatedRecord,
|
||||
@@ -163,15 +176,30 @@
|
||||
return updatedRecord;
|
||||
});
|
||||
// Use new pokédex-scoped endpoint
|
||||
await fetch(`/api/pokedexes/${pokedexId}/catch-records`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(catchRecordsToUpdate)
|
||||
}).then(async () => {
|
||||
await getData(false);
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`/api/pokedexes/${pokedexId}/catch-records`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(catchRecordsToUpdate)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Server response:', response.status, errorText);
|
||||
alert(
|
||||
`Failed to update catch records: ${response.status} ${response.statusText}\n${errorText}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await getData({ page: currentPage, perPage: itemsPerPage, setCombinedDataToNull: false });
|
||||
} catch (error) {
|
||||
console.error('Error updating catch records:', error);
|
||||
alert(
|
||||
`Network error updating catch records: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function markBoxAsNotCaught(boxNumber: number) {
|
||||
@@ -204,6 +232,7 @@
|
||||
}
|
||||
|
||||
async function createCatchRecords() {
|
||||
if (!pokedexId) return;
|
||||
// this user doesn't have any catch records, so we need to create them
|
||||
await getPokedexEntries().then(async (pokedexEntries) => {
|
||||
// If there are no catch records, make one for each pokedex entry
|
||||
@@ -250,27 +279,22 @@
|
||||
|
||||
creatingRecords = false;
|
||||
failedToLoad = false;
|
||||
await getData();
|
||||
await getData({ page: currentPage, perPage: itemsPerPage });
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await getData();
|
||||
});
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
currentPage, itemsPerPage, getData();
|
||||
}
|
||||
}
|
||||
// Fetch data whenever pagination controls change (client-side only)
|
||||
$: if (browser && pokedexId) getData({ page: currentPage, perPage: itemsPerPage });
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pokedex.name} - Living Dex Tracker</title>
|
||||
<title>{pokedex ? `${pokedex.name} - Living Dex Tracker` : 'Pokédex - Living Dex Tracker'}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if !localUser}
|
||||
<p>Please <a href="/signin" class="underline text-primary hover:text-secondary">sign in</a></p>
|
||||
{:else if !pokedex}
|
||||
<p>Loading...</p>
|
||||
{:else}
|
||||
<div class="container mx-auto p-4 max-w-screen-2xl">
|
||||
<!-- Pokédex Header -->
|
||||
@@ -285,32 +309,64 @@
|
||||
<!-- Type badges -->
|
||||
{#if pokedex.isLivingDex}
|
||||
<div class="badge badge-primary badge-lg gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Living
|
||||
</div>
|
||||
{/if}
|
||||
{#if pokedex.isShinyDex}
|
||||
<div class="badge badge-secondary badge-lg gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
|
||||
/>
|
||||
</svg>
|
||||
Shiny
|
||||
</div>
|
||||
{/if}
|
||||
{#if pokedex.isOriginDex}
|
||||
<div class="badge badge-accent badge-lg gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Origin
|
||||
</div>
|
||||
{/if}
|
||||
{#if pokedex.isFormDex}
|
||||
<div class="badge badge-info badge-lg gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"
|
||||
/>
|
||||
</svg>
|
||||
Form
|
||||
</div>
|
||||
@@ -320,17 +376,35 @@
|
||||
{#if pokedex.gameScope}
|
||||
<div class="divider divider-horizontal mx-0"></div>
|
||||
<div class="flex items-center gap-2 text-base-content/70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-semibold">{pokedex.gameScope}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="divider divider-horizontal mx-0"></div>
|
||||
<div class="flex items-center gap-2 text-base-content/70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 011.523-1.943A5.977 5.977 0 0116 10c0 .34-.028.675-.083 1H15a2 2 0 00-2 2v2.197A5.973 5.973 0 0110 16v-2a2 2 0 00-2-2 2 2 0 01-2-2 2 2 0 00-1.668-1.973z" clip-rule="evenodd" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 011.523-1.943A5.977 5.977 0 0116 10c0 .34-.028.675-.083 1H15a2 2 0 00-2 2v2.197A5.973 5.973 0 0110 16v-2a2 2 0 00-2-2 2 2 0 01-2-2 2 2 0 00-1.668-1.973z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-semibold">All Games</span>
|
||||
</div>
|
||||
@@ -345,8 +419,15 @@
|
||||
<!-- Right side: Actions -->
|
||||
<div class="flex flex-row lg:flex-col gap-2">
|
||||
<a href="/my-pokedexes" class="btn btn-outline btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
My Pokédexes
|
||||
</a>
|
||||
@@ -358,7 +439,6 @@
|
||||
<!-- Box View -->
|
||||
<PokedexViewBoxes
|
||||
{showShiny}
|
||||
showForms={pokedex.isFormDex}
|
||||
bind:combinedData
|
||||
bind:boxNumbers
|
||||
bind:creatingRecords
|
||||
@@ -382,7 +462,7 @@
|
||||
showForms={pokedex.isFormDex}
|
||||
{showShiny}
|
||||
userId={localUser?.id}
|
||||
pokedexId={pokedexId}
|
||||
{pokedexId}
|
||||
on:updateCatch={handleModalCatchUpdate}
|
||||
/>
|
||||
</PokedexModal>
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let showSuccess = false;
|
||||
|
||||
onMount(() => {
|
||||
// Trigger success animation after component mounts
|
||||
setTimeout(() => {
|
||||
showSuccess = true;
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Welcome - Living Dex Tracker</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Welcome to Living Dex Tracker! Check your email to verify your account."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-[calc(100vh-16rem)] bg-base-100 py-8 md:py-16 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Success Badge -->
|
||||
{#if showSuccess}
|
||||
<div class="flex justify-center mb-8 animate-fade-in">
|
||||
<div class="badge badge-success badge-lg gap-2 p-6 shadow-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-lg font-semibold">Account Created Successfully!</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Hero Content -->
|
||||
<div class="hero">
|
||||
<div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 w-full">
|
||||
<!-- Main Content -->
|
||||
<div class="text-center lg:text-left flex-1 space-y-6">
|
||||
<h1
|
||||
class="text-4xl md:text-5xl lg:text-6xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
Welcome to Living Dex Tracker!
|
||||
</h1>
|
||||
|
||||
<div class="space-y-4 text-base md:text-lg">
|
||||
<!-- Email Verification Section with Icon -->
|
||||
<div class="flex items-start gap-4 p-4 bg-info/10 rounded-lg border-l-4 border-info">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-info animate-pulse"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-info mb-1">Check Your Email</p>
|
||||
<p class="text-sm opacity-90">
|
||||
We've sent you a magic link to verify your account. Click the link in your email
|
||||
to sign in and start tracking your Pokédex progress.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="opacity-80">
|
||||
After clicking the link, you'll be automatically signed in and can start tracking your
|
||||
Pokémon catches across all generations!
|
||||
</p>
|
||||
|
||||
<!-- Help Section -->
|
||||
<div class="pt-4">
|
||||
<p class="text-sm opacity-70">
|
||||
<span class="font-semibold">Haven't received the email?</span> Check your spam or junk
|
||||
folder.
|
||||
</p>
|
||||
<p class="text-sm opacity-70 mt-2">
|
||||
Still having trouble?
|
||||
<a
|
||||
href="mailto:support@example.com"
|
||||
class="link link-primary font-semibold hover:underline"
|
||||
>
|
||||
Contact support
|
||||
</a> for assistance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What's Next Card -->
|
||||
<div
|
||||
class="card w-full max-w-md shadow-2xl bg-neutral hover:shadow-3xl transition-shadow duration-300"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4 flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
What's Next?
|
||||
</h2>
|
||||
|
||||
<ul class="space-y-4">
|
||||
<li class="flex items-start gap-3 group">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-success flex items-center justify-center group-hover:scale-110 transition-transform"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-success-content"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold">Check your email</p>
|
||||
<p class="text-sm opacity-70">Look for our verification email in your inbox</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="flex items-start gap-3 group">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-success flex items-center justify-center group-hover:scale-110 transition-transform"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-success-content"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold">Click the magic link</p>
|
||||
<p class="text-sm opacity-70">This will sign you in and verify your account</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="flex items-start gap-3 group">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-success flex items-center justify-center group-hover:scale-110 transition-transform"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-success-content"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold">Start tracking your Pokémon</p>
|
||||
<p class="text-sm opacity-70">
|
||||
Create your first Pokédex and begin your journey!
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Additional Info -->
|
||||
<div class="mt-6 pt-6 border-t border-base-300">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>The verification link expires in 24 hours</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Features Preview -->
|
||||
<div class="mt-16 text-center">
|
||||
<h3 class="text-2xl font-bold mb-6">What You Can Do With Living Dex Tracker</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="card bg-base-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-12 w-12 text-primary mb-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h4 class="font-semibold">Track Multiple Pokédexes</h4>
|
||||
<p class="text-sm opacity-70">
|
||||
Manage different Living Dex challenges across all generations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-12 w-12 text-secondary mb-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
<h4 class="font-semibold">Share Your Progress</h4>
|
||||
<p class="text-sm opacity-70">Connect with friends and share your Pokédex journey</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-md hover:shadow-lg transition-shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-12 w-12 text-accent mb-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h4 class="font-semibold">Works Offline</h4>
|
||||
<p class="text-sm opacity-70">Track your catches even without an internet connection</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Respect user's motion preferences */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-fade-in,
|
||||
.animate-pulse {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.group-hover\:scale-110 {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gradient text fallback for older browsers */
|
||||
@supports not (background-clip: text) {
|
||||
h1 {
|
||||
background: none;
|
||||
color: hsl(var(--p));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -149,6 +149,7 @@ INSERT INTO pokedex_entries (
|
||||
(231, 'Phanpy', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
|
||||
(232, 'Donphan', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
|
||||
(232, 'Donphan', 'Female', false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
|
||||
(233, 'Porygon2', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
|
||||
(234, 'Stantler', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
|
||||
(235, 'Smeargle', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
|
||||
(236, 'Tyrogue', NULL, false, 'Johto', ARRAY['Gold', 'Silver', 'Crystal', 'HeartGold', 'SoulSilver']),
|
||||
@@ -256,6 +257,7 @@ INSERT INTO regional_dex_numbers (
|
||||
((SELECT id FROM pokedex_entries WHERE "pokedexNumber" = 230 AND form IS NULL), 'Johto', 79),
|
||||
((SELECT id FROM pokedex_entries WHERE "pokedexNumber" = 231 AND form IS NULL), 'Johto', 80),
|
||||
((SELECT id FROM pokedex_entries WHERE "pokedexNumber" = 232 AND form IS NULL), 'Johto', 81),
|
||||
((SELECT id FROM pokedex_entries WHERE "pokedexNumber" = 233 AND form IS NULL), 'Johto', 82),
|
||||
((SELECT id FROM pokedex_entries WHERE "pokedexNumber" = 234 AND form IS NULL), 'Johto', 83),
|
||||
((SELECT id FROM pokedex_entries WHERE "pokedexNumber" = 235 AND form IS NULL), 'Johto', 84),
|
||||
((SELECT id FROM pokedex_entries WHERE "pokedexNumber" = 236 AND form IS NULL), 'Johto', 85),
|
||||
@@ -279,4 +281,4 @@ INSERT INTO regional_dex_numbers (
|
||||
INSERT INTO metadata (key, value) VALUES
|
||||
('johto_seeded', 'true'),
|
||||
('johto_seed_date', NOW()::TEXT),
|
||||
('johto_pokemon_count', '98');
|
||||
('johto_pokemon_count', '99');
|
||||
|
||||
Reference in New Issue
Block a user