feat(#64): Improve the about page

This commit is contained in:
Josh Creek
2026-01-10 15:27:00 +00:00
parent 463ab88545
commit adc96baa90
12 changed files with 586 additions and 535 deletions
@@ -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
@@ -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;
$$;
@@ -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;
$$;
@@ -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;
$$;