diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
new file mode 100644
index 0000000..30a885e
--- /dev/null
+++ b/src/routes/+page.server.ts
@@ -0,0 +1,17 @@
+import type { PageServerLoad } from './$types';
+
+/**
+ * Server-side load function for the homepage
+ *
+ * Fetches public statistics from the database and passes them to the page component.
+ * Stats are cached for 24 hours to improve performance.
+ */
+export const load: PageServerLoad = async ({ fetch }) => {
+ // Fetch stats from database
+ const statsResponse = await fetch('/api/stats');
+ const statsData = await statsResponse.json();
+
+ return {
+ stats: statsData.error ? null : statsData
+ };
+};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index e7d9821..6a61202 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -5,137 +5,346 @@
import SignUp from '$lib/components/SignUp.svelte';
export let data;
- let { supabase } = data;
- $: ({ supabase } = data);
+ let { supabase, stats } = data;
+ $: ({ supabase, stats } = data);
let localUser: User | null;
const unsubscribe = user.subscribe((value) => {
localUser = value;
});
onDestroy(unsubscribe);
+
+ // Format large numbers (e.g., 1000 -> 1K, 1000000 -> 1M)
+ function formatNumber(num: number): string {
+ if (num >= 1000000) {
+ return (num / 1000000).toFixed(1).replace(/.0$/, '') + 'M';
+ }
+ if (num >= 1000) {
+ return (num / 1000).toFixed(0) + 'K';
+ }
+ return num.toString();
+ }
+
+ // Get formatted stats or fallback to 0
+ let pokemonCaught = '0';
+ let users = '0';
+ let livingDexesCompleted = '0';
+ $: {
+ pokemonCaught = stats ? formatNumber(stats.pokemonCaught) : '0';
+ users = stats ? formatNumber(stats.users) : '0';
+ livingDexesCompleted = stats ? formatNumber(stats.livingDexesCompleted) : '0';
+ }
+
+ Living Dex Tracker - Track Your Pokédex Journey
+
+
+
+
-
Start Your Pokédex Journey!
+
+
+
+ Free
+
+
+
+ Open Source
+
+
+
+ Offline-friendly
+
+
+
Start Your Pokédex Journey!
+
+ Track your progress towards a complete Living Pokédex — one of every Pokémon, actively
+ maintained across your boxes. Join thousands of trainers worldwide.
+
+ A Living Dex is a complete Pokédex where you keep one of every Pokémon in your boxes
+ (often including forms/variants). Living Dex Tracker helps you build and maintain that
+ collection with filters, notes, and progress tracking.
+
+
+
+
+
+
+
+
+
+
Why Choose Living Dex Tracker?
+
+
+
+
+
+
+
+
+
+
Social & Shareable
+
+ Easily share your Pokédex journey with friends, or find theirs. If you'd rather go
+ it alone, that's okay too!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Free & Open Source
+
+ Completely open source and free to use, enabling the community to contribute updates
+ as soon as new Pokémon are released.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Advanced Filtering
+
+ Track simple progress or tackle harder variants like a Living Origin Form Dex with
+ our powerful filtering options for targeted catching sessions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
100% free
+
+ Did we mention it's completely free to use? Oh, we did? Good. Because it is.
+
+
+
+
+
+
+
+
+
+
+
+
+
Ready to Start Your Journey?
+
+ Get started today with tracking you Living Pokédex progress. It's free, open source, and built
+ with love for the Pokémon community.
+
+
+
+
+
Sign Up Now
+
+
+
+
+
+
+
+
+
+
+
+
+
Legal Disclaimer
+
+ Living Dex Tracker is a fan-made project. We do not claim ownership of any Pokémon
+ characters, images, or other content featured on this website. This project is not
+ affiliated with, endorsed, sponsored, or specifically approved by Nintendo, Game Freak, or
+ The Pokémon Company.
+
+
-
-
-
New Users
-
4,200
-
↗︎ 400 (22%)
-
-
-
-
Living Dexes Completed
-
1,200
-
↘︎ 90 (14%)
-
-
-
-
-
Features
-
-
-
-
-
-
-
Social
-
- Easily share your Pokédex journey with friends, or find theirs. If you'd rather go it
- alone, that's okay too!
-
-
-
-
-
-
-
-
-
Free & Open Source
-
- Completely open source and free to use, enabling the community to contribute updates
- as soon as new Pokémon are released.
-
-
-
-
-
-
-
-
-
Easy to use with advanced filtering
-
- If you just want to track your progress completing your Pokédex, you can do that. But
- if you are attempting a harder variant, like a Living Origin Forme Dex, you might find
- our advanced filtering options helpful for tracking target Pokémon for specific
- catching sessions.
-
-
-
-
-
-
-
-
-
100% free
-
Did we mention it's completely free to use? Oh, we did? Good. Because it is.
-
- Living Dex Tracker is dolor sit amet, consectetur adipiscing elit. Proin dignissim risus eu quam
- feugiat malesuada. Vivamus arcu sapien, feugiat sed lacus ut, malesuada congue nibh. In lacinia
- lacus quis bibendum pulvinar. Aliquam in suscipit risus. Aliquam ut justo dolor.
-
-
-
Features
-
-
-
-
-
-
-
-
Player Dex Search
-
Find and explore your friends' Pokémon collections effortlessly.
-
-
-
-
-
Meet The Team
-
-
Team Member 1
-
-
-
-
-
-
-
-
-
-
Team Member 2
-
-
-
-
-
The Developement
-
- Share insights into the development process, challenges faced, and any interesting anecdotes
- or milestones achieved during the project's journey.
-
- Living Dex Tracker is a fan-made website dedicated to providing information about Pokemon
- creatures. We do not claim ownership of any Pokemon characters, images, or other content
- featured on this website.
-
-
- We do not generate revenue from the use of Pokemon content. Any advertisements or sponsored
- content on this website are unrelated to Pokemon and are used solely to support the
- maintenance and operation of the website.
-
-
-
-
-
Acknowledgments
-
- Express gratitude to any individuals or organisations that have contributed to the project's
- success, including sponsors, supporters, or open-source contributors.
-
-
-
-
-
-
diff --git a/src/routes/about/+page.ts b/src/routes/about/+page.ts
deleted file mode 100644
index 3e13462..0000000
--- a/src/routes/about/+page.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { dev } from '$app/environment';
-
-// we don't need any JS on this page, though we'll load
-// it in dev so that we get hot module replacement...
-export const csr = dev;
-
-// since there's no dynamic data here, we can prerender
-// it so that it gets served as a static asset in prod
-export const prerender = true;
diff --git a/src/routes/api/stats/+server.ts b/src/routes/api/stats/+server.ts
new file mode 100644
index 0000000..4fa83f8
--- /dev/null
+++ b/src/routes/api/stats/+server.ts
@@ -0,0 +1,40 @@
+import { json } from '@sveltejs/kit';
+import type { RequestEvent } from '@sveltejs/kit';
+
+/**
+ * GET /api/stats
+ *
+ * Returns public statistics about the application:
+ * - pokemonCaught: Total count of caught Pokémon
+ * - users: Total count of users
+ * - livingDexesCompleted: Total count of completed pokedexes
+ *
+ * Stats are cached for 24 hours to improve performance.
+ * The first request after 24 hours will trigger a cache refresh.
+ */
+export const GET = async (event: RequestEvent) => {
+ try {
+ // Call the database function to get stats (with caching)
+ const { data, error } = await event.locals.supabase.rpc('get_public_stats');
+
+ if (error) {
+ console.error('Error fetching stats:', error);
+ return json({ error: 'Failed to fetch stats' }, { status: 500 });
+ }
+
+ if (!data || data.length === 0) {
+ return json({ error: 'No stats data found' }, { status: 404 });
+ }
+
+ const stats = data[0];
+ return json({
+ pokemonCaught: stats.pokemon_caught,
+ users: stats.total_users,
+ livingDexesCompleted: stats.completed_pokedexes,
+ updatedAt: stats.updated_at
+ });
+ } catch (err) {
+ console.error('Error in GET /api/stats:', err);
+ return json({ error: 'Internal Server Error' }, { status: 500 });
+ }
+};
diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte
deleted file mode 100644
index dfb351d..0000000
--- a/src/routes/profile/+page.svelte
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
- {#if localUser}
-
-
-
-
-
-
-
-
-
{localUser?.email}
-
Title
-
Level
-
-
-
-
-
70%
-
-
Total Pokemon Caught
-
-
-
Types Collected
-
-
Fire
-
-
- any other relevant statistics or achievements
-
-
- theme selection
-
- profile background
-
- any other features that enhance user experience and personalisation
-
-
- options for users to share their profile or achievements on social media platforms,
- encouraging engagement and interaction with other users
-
-
- {:else}
-
Please log in to view this page
- {/if}
-
-
-
diff --git a/supabase/migrations/20260110120000_create_stats_cache.sql b/supabase/migrations/20260110120000_create_stats_cache.sql
new file mode 100644
index 0000000..96c380a
--- /dev/null
+++ b/supabase/migrations/20260110120000_create_stats_cache.sql
@@ -0,0 +1,22 @@
+-- Create stats_cache table to store cached statistics
+-- This table will be updated once per day to improve performance
+
+CREATE TABLE stats_cache (
+ id BIGSERIAL PRIMARY KEY,
+ pokemon_caught BIGINT NOT NULL DEFAULT 0,
+ total_users BIGINT NOT NULL DEFAULT 0,
+ completed_pokedexes BIGINT NOT NULL DEFAULT 0,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Create index for faster lookups
+CREATE INDEX idx_stats_cache_updated_at ON stats_cache(updated_at DESC);
+
+-- Enable RLS
+ALTER TABLE stats_cache ENABLE ROW LEVEL SECURITY;
+
+-- RLS policies: Only allow reads (stats are public)
+CREATE POLICY "Anyone can view stats cache" ON stats_cache
+ FOR SELECT USING (true);
+
+-- No insert/update/delete policies - only database functions can modify this table
diff --git a/supabase/migrations/20260110120001_create_update_stats_function.sql b/supabase/migrations/20260110120001_create_update_stats_function.sql
new file mode 100644
index 0000000..6d9c285
--- /dev/null
+++ b/supabase/migrations/20260110120001_create_update_stats_function.sql
@@ -0,0 +1,51 @@
+-- Create function to calculate and update stats in the cache table
+-- This function runs the expensive queries and stores the results
+
+CREATE OR REPLACE FUNCTION update_and_get_stats()
+RETURNS TABLE (
+ pokemon_caught BIGINT,
+ total_users BIGINT,
+ completed_pokedexes BIGINT,
+ updated_at TIMESTAMPTZ
+)
+SECURITY DEFINER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_pokemon_caught BIGINT;
+ v_total_users BIGINT;
+ v_completed_pokedexes BIGINT;
+BEGIN
+ -- Calculate current stats: count of caught Pokémon
+ SELECT COUNT(*) INTO v_pokemon_caught
+ FROM catch_records
+ WHERE caught = true;
+
+ -- Calculate current stats: count of total users
+ SELECT COUNT(*) INTO v_total_users
+ FROM auth.users;
+
+ -- Calculate current stats: count of completed pokedexes
+ -- A pokedex is completed when all its entries have caught = true
+ SELECT COUNT(*) INTO v_completed_pokedexes
+ FROM (
+ SELECT pokedexId
+ FROM catch_records
+ GROUP BY pokedexId
+ HAVING COUNT(*) = COUNT(*) FILTER (WHERE caught = true)
+ ) completed;
+
+ -- Update cache using UPSERT pattern
+ INSERT INTO stats_cache (pokemon_caught, total_users, completed_pokedexes, updated_at)
+ VALUES (v_pokemon_caught, v_total_users, v_completed_pokedexes, NOW())
+ ON CONFLICT (id) DO UPDATE
+ SET pokemon_caught = EXCLUDED.pokemon_caught,
+ total_users = EXCLUDED.total_users,
+ completed_pokedexes = EXCLUDED.completed_pokedexes,
+ updated_at = NOW();
+
+ -- Return the stats
+ RETURN QUERY
+ SELECT v_pokemon_caught, v_total_users, v_completed_pokedexes, NOW() AS updated_at;
+END;
+$$;
diff --git a/supabase/migrations/20260110120002_create_get_stats_function.sql b/supabase/migrations/20260110120002_create_get_stats_function.sql
new file mode 100644
index 0000000..55dfff8
--- /dev/null
+++ b/supabase/migrations/20260110120002_create_get_stats_function.sql
@@ -0,0 +1,36 @@
+-- Create function to get public stats with caching
+-- Returns cached stats if fresh (within 24 hours), otherwise refreshes cache
+
+CREATE OR REPLACE FUNCTION get_public_stats()
+RETURNS TABLE (
+ pokemon_caught BIGINT,
+ total_users BIGINT,
+ completed_pokedexes BIGINT,
+ updated_at TIMESTAMPTZ
+)
+SECURITY DEFINER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_cache_exists BOOLEAN;
+BEGIN
+ -- Check if cache exists and is fresh (within 24 hours)
+ SELECT EXISTS(
+ SELECT 1 FROM stats_cache
+ WHERE stats_cache.updated_at > NOW() - INTERVAL '24 hours'
+ ) INTO v_cache_exists;
+
+ IF v_cache_exists THEN
+ -- Return cached stats
+ RETURN QUERY
+ SELECT sc.pokemon_caught, sc.total_users, sc.completed_pokedexes, sc.updated_at
+ FROM stats_cache sc
+ ORDER BY sc.updated_at DESC
+ LIMIT 1;
+ ELSE
+ -- Cache is stale or doesn't exist, update it
+ RETURN QUERY
+ SELECT * FROM update_and_get_stats();
+ END IF;
+END;
+$$;
diff --git a/supabase/migrations/20260110144000_fix_stats_functions_ambiguity.sql b/supabase/migrations/20260110144000_fix_stats_functions_ambiguity.sql
new file mode 100644
index 0000000..b557244
--- /dev/null
+++ b/supabase/migrations/20260110144000_fix_stats_functions_ambiguity.sql
@@ -0,0 +1,89 @@
+-- Fix ambiguous column reference errors in stats functions.
+--
+-- If earlier migrations have already been applied, editing them will not update
+-- the deployed database. This migration re-defines the functions using
+-- CREATE OR REPLACE so the corrected definitions take effect.
+
+CREATE OR REPLACE FUNCTION update_and_get_stats()
+RETURNS TABLE (
+ pokemon_caught BIGINT,
+ total_users BIGINT,
+ completed_pokedexes BIGINT,
+ updated_at TIMESTAMPTZ
+)
+SECURITY DEFINER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_pokemon_caught BIGINT;
+ v_total_users BIGINT;
+ v_completed_pokedexes BIGINT;
+BEGIN
+ -- Calculate current stats: count of caught Pokémon
+ SELECT COUNT(*) INTO v_pokemon_caught
+ FROM catch_records
+ WHERE caught = true;
+
+ -- Calculate current stats: count of total users
+ SELECT COUNT(*) INTO v_total_users
+ FROM auth.users;
+
+ -- Calculate current stats: count of completed pokedexes
+ -- A pokedex is completed when all its entries have caught = true
+ SELECT COUNT(*) INTO v_completed_pokedexes
+ FROM (
+ SELECT "pokedexId"
+ FROM catch_records
+ GROUP BY "pokedexId"
+ HAVING COUNT(*) = COUNT(*) FILTER (WHERE caught = true)
+ ) completed;
+
+ -- Update cache using UPSERT pattern
+ INSERT INTO stats_cache (pokemon_caught, total_users, completed_pokedexes, updated_at)
+ VALUES (v_pokemon_caught, v_total_users, v_completed_pokedexes, NOW())
+ ON CONFLICT (id) DO UPDATE
+ SET pokemon_caught = EXCLUDED.pokemon_caught,
+ total_users = EXCLUDED.total_users,
+ completed_pokedexes = EXCLUDED.completed_pokedexes,
+ updated_at = NOW();
+
+ -- Return stats
+ RETURN QUERY
+ SELECT v_pokemon_caught, v_total_users, v_completed_pokedexes, NOW() AS updated_at;
+END;
+$$;
+
+
+CREATE OR REPLACE FUNCTION get_public_stats()
+RETURNS TABLE (
+ pokemon_caught BIGINT,
+ total_users BIGINT,
+ completed_pokedexes BIGINT,
+ updated_at TIMESTAMPTZ
+)
+SECURITY DEFINER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_cache_exists BOOLEAN;
+BEGIN
+ -- Check if cache exists and is fresh (within 24 hours)
+ SELECT EXISTS(
+ SELECT 1 FROM stats_cache
+ WHERE stats_cache.updated_at > NOW() - INTERVAL '24 hours'
+ ) INTO v_cache_exists;
+
+ IF v_cache_exists THEN
+ -- Return cached stats (qualify updated_at to avoid ambiguity with output parameter)
+ RETURN QUERY
+ SELECT sc.pokemon_caught, sc.total_users, sc.completed_pokedexes, sc.updated_at
+ FROM stats_cache sc
+ ORDER BY sc.updated_at DESC
+ LIMIT 1;
+ ELSE
+ -- Cache is stale or doesn't exist, update it
+ RETURN QUERY
+ SELECT * FROM update_and_get_stats();
+ END IF;
+END;
+$$;
From 7c68d121cedcbe22f87c9e603f7f5483ed96809f Mon Sep 17 00:00:00 2001
From: Josh Creek <8179928+jcreek@users.noreply.github.com>
Date: Sat, 10 Jan 2026 16:27:23 +0000
Subject: [PATCH 2/6] fix(#64): Address PR comments
---
src/lib/components/SignUp.svelte | 4 ++--
src/routes/+page.svelte | 14 +++++++-------
...20260110120001_create_update_stats_function.sql | 14 ++++++++------
...0260110144000_fix_stats_functions_ambiguity.sql | 9 +++++----
4 files changed, 22 insertions(+), 19 deletions(-)
diff --git a/src/lib/components/SignUp.svelte b/src/lib/components/SignUp.svelte
index 8dfd226..09951f4 100644
--- a/src/lib/components/SignUp.svelte
+++ b/src/lib/components/SignUp.svelte
@@ -54,11 +54,11 @@
/>