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
+6
View File
@@ -57,3 +57,9 @@
<button class="btn btn btn-primary" on:click={signUpNewUser}>Sign Up</button>
</div>
</form>
<style scoped>
.card-body {
padding: 0.5rem;
}
</style>
+2 -7
View File
@@ -91,7 +91,7 @@
<header>
<div class="navbar bg-primary text-primary-content">
<div class="navbar-start">
<div class="dropdown">
<!-- <div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -110,12 +110,11 @@
<ul
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-primary text-primary-content rounded-box w-52"
>
<li><a href="/about">About</a></li>
<li>
<a href="https://github.com/jcreek/LivingDexTracker" target="_blank">Contribute</a>
</li>
</ul>
</div>
</div> -->
</div>
<div class="navbar-center">
<a class="btn btn-ghost text-xl" href="/">Living Dex Tracker</a>
@@ -151,10 +150,6 @@
<li>
<a href="/my-pokedexes"> My Pokédexes </a>
</li>
<li>
<a href="/profile"> Profile </a>
</li>
<li><a href="/settings">Settings</a></li>
<li><SignOut {supabase} on:signedOut={getUser} /></li>
{:else}
<li><SignIn {supabase} on:signedIn={getUser} /></li>
+17
View File
@@ -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
};
};
+323 -114
View File
@@ -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';
}
</script>
<svelte:head>
<title>Living Dex Tracker - Track Your Pokédex Journey</title>
<meta
name="description"
content="A free, open source tool to track your Living Pokédex progress. Join thousands of trainers worldwide in completing their collection."
/>
</svelte:head>
<!-- Hero Section -->
<div class="hero bg-base-100 my-36">
<div class="hero-content flex-col lg:flex-row-reverse">
<div class="text-center lg:text-left">
<h1 class="text-5xl font-bold">Start Your Pokédex Journey!</h1>
<div class="flex flex-wrap gap-2 mb-4">
<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>
Free
</div>
<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>
Open Source
</div>
<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="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>
Offline-friendly
</div>
</div>
<h1 class="text-5xl font-bold mb-6">Start Your Pokédex Journey!</h1>
<p class="text-xl mb-6 text-base-content/80">
Track your progress towards a complete Living Pokédex — one of every Pokémon, actively
maintained across your boxes. Join thousands of trainers worldwide.
</p>
<div class="flex flex-wrap gap-3 justify-center lg:justify-start">
<a
href="https://discord.gg/2ytj4pkUPY"
target="_blank"
rel="noopener noreferrer"
class="btn btn-primary btn-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
/>
</svg>
Join Discord
</a>
<a
href="https://github.com/jcreek/LivingDexTracker"
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline btn-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>
Contribute on GitHub
</a>
</div>
</div>
<div class="card shrink-0 w-full max-w-sm shadow-2xl bg-neutral">
<SignUp {supabase} />
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Get Started Free</h2>
<SignUp {supabase} />
</div>
</div>
</div>
</div>
<div
class="md:container md:mx-auto justify-self-center flex justify-center items-center h-full flex-col mb-10"
>
<div class="stats stats-vertical lg:stats-horizontal shadow bg-neutral text-center">
<div class="stat">
<div class="stat-title">Pokémon caught</div>
<div class="stat-value">310K</div>
<div class="stat-desc">Since January 2024</div>
<!-- Stats Section -->
<div class="bg-base-200 py-16">
<div class="container mx-auto px-4">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-neutral text-center w-full">
<div class="stat">
<div class="stat-title">Pokémon caught</div>
<div class="stat-value text-primary">{pokemonCaught}</div>
<div class="stat-desc">Since January 2026</div>
</div>
<div class="stat">
<div class="stat-title">Users</div>
<div class="stat-value text-primary">{users}</div>
<div class="stat-desc">↗︎ 400 (22%)</div>
</div>
<div class="stat">
<div class="stat-title">Living Dexes Completed</div>
<div class="stat-value text-primary">{livingDexesCompleted}</div>
<div class="stat-desc">Trainers worldwide</div>
</div>
</div>
</div>
</div>
<!-- What is a Living Dex Section -->
<div class="py-16 bg-base-100">
<div class="container mx-auto px-4 max-w-4xl">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-3xl mb-4">What is a Living Dex?</h2>
<p class="text-lg text-base-content/80">
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.
</p>
</div>
</div>
</div>
</div>
<!-- Features Section -->
<div class="py-16 bg-base-200">
<div class="container mx-auto px-4 max-w-6xl">
<h1 class="text-4xl font-bold mb-12 text-center">Why Choose Living Dex Tracker?</h1>
<div class="grid md:grid-cols-2 gap-6">
<div class="card bg-neutral shadow-xl">
<div class="card-body">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<svg
class="w-10 h-10 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<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"
></path>
</svg>
</div>
<div>
<h2 class="card-title text-xl mb-2">Social & Shareable</h2>
<p class="text-base-content/80">
Easily share your Pokédex journey with friends, or find theirs. If you'd rather go
it alone, that's okay too!
</p>
</div>
</div>
</div>
</div>
<div class="card bg-neutral shadow-xl">
<div class="card-body">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<svg
class="w-10 h-10 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
></path>
</svg>
</div>
<div>
<h2 class="card-title text-xl mb-2">Free & Open Source</h2>
<p class="text-base-content/80">
Completely open source and free to use, enabling the community to contribute updates
as soon as new Pokémon are released.
</p>
</div>
</div>
</div>
</div>
<div class="card bg-neutral shadow-xl">
<div class="card-body">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<svg
class="w-10 h-10 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
></path>
</svg>
</div>
<div>
<h2 class="card-title text-xl mb-2">Advanced Filtering</h2>
<p class="text-base-content/80">
Track simple progress or tackle harder variants like a Living Origin Form Dex with
our powerful filtering options for targeted catching sessions.
</p>
</div>
</div>
</div>
</div>
<div class="card bg-neutral shadow-xl">
<div class="card-body">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<svg
class="w-10 h-10 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<div>
<h2 class="card-title text-xl mb-2">100% free</h2>
<p class="text-base-content/80">
Did we mention it's completely free to use? Oh, we did? Good. Because it is.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CTA Section -->
<div class="py-16 bg-base-100">
<div class="container mx-auto px-4 text-center">
<h2 class="text-4xl font-bold mb-6">Ready to Start Your Journey?</h2>
<p class="text-xl mb-8 text-base-content/80 max-w-2xl mx-auto">
Get started today with tracking you Living Pokédex progress. It's free, open source, and built
with love for the Pokémon community.
</p>
<div class="flex flex-wrap gap-4 justify-center">
<div class="card shrink-0 w-full max-w-sm shadow-2xl bg-neutral">
<div class="card-body">
<h3 class="card-title text-xl mb-4">Sign Up Now</h3>
<SignUp {supabase} />
</div>
</div>
</div>
</div>
</div>
<!-- Legal Section -->
<div class="py-8 bg-base-200">
<div class="container mx-auto px-4 max-w-4xl">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-lg mb-2">Legal Disclaimer</h2>
<p class="text-sm text-base-content/70">
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.
</p>
</div>
</div>
<div class="stat">
<div class="stat-title">New Users</div>
<div class="stat-value">4,200</div>
<div class="stat-desc">↗︎ 400 (22%)</div>
</div>
<div class="stat">
<div class="stat-title">Living Dexes Completed</div>
<div class="stat-value">1,200</div>
<div class="stat-desc">↘︎ 90 (14%)</div>
</div>
</div>
<div class="features mt-8">
<h1 class="flex justify-center text-4xl font-bold mb-5">Features</h1>
<ul class="space-y-6">
<li class="p-6 rounded-lg shadow-md bg-neutral">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">Social</h2>
<p>
Easily share your Pokédex journey with friends, or find theirs. If you'd rather go it
alone, that's okay too!
</p>
</div>
</div>
</li>
<li class="p-6 rounded-lg shadow-md bg-neutral">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">Free & Open Source</h2>
<p>
Completely open source and free to use, enabling the community to contribute updates
as soon as new Pokémon are released.
</p>
</div>
</div>
</li>
<li class="p-6 rounded-lg shadow-md bg-neutral">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">Easy to use with advanced filtering</h2>
<p>
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.
</p>
</div>
</div>
</li>
<li class="p-6 rounded-lg shadow-md bg-neutral">
<div class="flex items-start space-x-4">
<svg
class="w-8 h-8 text-green-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
></path>
</svg>
<div>
<h2 class="text-lg font-semibold">100% free</h2>
<p>Did we mention it's completely free to use? Oh, we did? Good. Because it is.</p>
</div>
</div>
</li>
</ul>
</div>
</div>
-327
View File
@@ -1,327 +0,0 @@
<svelte:head>
<title>About Us</title>
<meta name="description" content="About this app" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400..700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Staatliches&display=swap"
rel="stylesheet"
/>
</svelte:head>
<div class="content h-full bg-base inline-block">
<div id="grad">
<div id="banner-content">
<p class=" text-xs opacity-70 tracking-wide">Learn</p>
<h2 class=" mt-0 pt-0 text-5xl tracking-widest text-center font-bold p-24">
About<br /> Us
</h2>
</div>
</div>
<div id="introduction">
<p class="font-semibold text-xl mb-5 ml-5">
Introducing <span class="typewriter-text underline decoration-accent">Living Dex Tracker</span
>
</p>
<p class="text-base mt-10 mb-5 ml-5 mr-5">
<img src="/placeholder-bulb.png" alt="bulb" class="float-right w-28" />
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.
</p>
<p class="font-semibold text-xl mt-20 mb-5 ml-5">Features</p>
<!-- TODO (#41) - Make this a card carousel -->
<div class="card md:card-side bg-base-00 shadow-m">
<figure>
<img src="/OIG3.jpeg" alt="Album" />
</figure>
<div class="card-body">
<p class="card-title">Player Dex Search</p>
<p>Find and explore your friends' Pokémon collections effortlessly.</p>
</div>
</div>
<div class="bg-neutral pb-20">
<p class="font-semibold text-white text-xl mt-0 mb-5 ml-5">Meet The Team</p>
<div class="avatar flex items-center justify-end mb-20">
<p class="text-m mb-5 ml-5 mr-5">Team Member 1</p>
<div class="w-24 rounded-full flex justify-end">
<img src="/OIG5.jpg" alt="profile" />
</div>
</div>
<div class="avatar">
<div class="w-24 rounded-full">
<img src="/OIG5.jpg" alt="profile " />
</div>
<p class="text-m mb-5 ml-5 mr-5">Team Member 2</p>
</div>
</div>
<div id="project-details">
<p class="font-semibold text-xl mt-10 mb-5 ml-5">The Developement</p>
<p class="text-sm mb-5 ml-5 mr-5">
Share insights into the development process, challenges faced, and any interesting anecdotes
or milestones achieved during the project's journey.
</p>
<ul class="timeline timeline-vertical">
<li>
<div class="timeline-start timeline-box">Example</div>
<div class="timeline-middle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 text-primary"
><path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd"
/></svg
>
</div>
<hr class="bg-primary" />
</li>
<li>
<hr class="bg-primary" />
<div class="timeline-middle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 text-primary"
><path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd"
/></svg
>
</div>
<div class="timeline-end timeline-box">Example</div>
<hr class="bg-primary" />
</li>
<li>
<hr class="bg-primary" />
<div class="timeline-start timeline-box">Example</div>
<div class="timeline-middle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 text-primary"
><path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd"
/></svg
>
</div>
<hr />
</li>
<li>
<hr />
<div class="timeline-middle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
><path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd"
/></svg
>
</div>
<div class="timeline-end timeline-box">Example</div>
<hr />
</li>
<li>
<hr />
<div class="timeline-start timeline-box">Example</div>
<div class="timeline-middle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
><path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd"
/></svg
>
</div>
</li>
</ul>
</div>
<p class="font-semibold text-xl mt-10 mb-5 ml-5">Join Us On Discord!</p>
<div id="contact-information ">
<a href="https://discord.gg/2ytj4pkUPY">
<button
class="relative inline-flex items-center justify-center w-16 p-0.5 ml-5 mb-20 me-2 overflow-hidden text-sm font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-cyan-500 to-blue-500 group-hover:from-cyan-500 group-hover:to-blue-500 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-cyan-200 dark:focus:ring-cyan-800"
>
<img src="/discord1.svg" class="w-12" /></button
>
</a>
</div>
<div class="contact-form">
<p class="font-semibold text-xl mt-0 mb-5 text-white pt-10">Contact Form</p>
<input type="text" name="first" placeholder="First Name" autocomplete="off" required />
<input type="text" name="last" placeholder="Last Name" autocomplete="off" required />
<input type="email" name="Email" placeholder="Email Address" autocomplete="off" required />
<textarea
rows="5"
cols="50"
name="message"
placeholder="Enter text"
autocomplete="off"
required
/>
<button type="submit" id="submit-button">Send Message</button>
</div>
<div id="legal-information">
<p class="font-semibold text-xl mt-0 mb-5 ml-5 text-white pt-10">Legal</p>
<p class="text-sm mb-5 ml-5 mr-5">
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.
</p>
<p class="text-sm mb-5 ml-5 mr-5">
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.
</p>
</div>
<div id="Acknowledgments">
<p class="font-semibold text-xl mt-0 mb-5 ml-5 text-white pt-10">Acknowledgments</p>
<p class="text-sm mb-5 ml-5 mr-5">
Express gratitude to any individuals or organisations that have contributed to the project's
success, including sponsors, supporters, or open-source contributors.
</p>
</div>
</div>
</div>
<style>
#banner-content {
padding-top: 170px;
}
#grad {
font-family: 'Pixelify Sans', sans-serif;
font-weight: 400;
font-style: normal;
width: 100%;
height: 422px;
margin: 0 auto;
background: linear-gradient(45deg, #d2001a, #7462ff, #f48e21, #23d5ab);
color: #fff;
background-size: 300% 300%;
animation: color 12s ease-in-out infinite;
text-align: center;
}
@keyframes color {
0% {
background-position: 0 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
#introduction .font-semibold {
font-family: 'Staatliches', sans-serif;
font-weight: 400;
font-style: normal;
font-size: 25px;
padding-top: 20px;
}
.avatar {
margin-left: 10px;
margin-right: 10px;
}
.contact-form {
background-color: rgb(216, 224, 178);
width: 380px;
height: 590px;
border-radius: 20px;
margin-bottom: 40px;
margin-left: 10px;
margin-right: 10px;
}
.contact-form > p {
margin-left: 9px;
}
input {
background-color: rgb(156, 186, 139);
width: 70%;
margin-left: 12%;
padding: 16px 16px;
border-radius: 10px;
border-color: rgb(131, 167, 124);
border-width: 2px;
margin-bottom: 20px;
font-family: 'Staatliches', sans-serif;
}
textarea {
background-color: rgb(156, 186, 139);
width: 70%;
margin-left: 12%;
padding: 16px 16px;
border-radius: 10px;
border-color: rgb(83, 145, 50);
margin-bottom: 25px;
font-family: 'Staatliches', sans-serif;
}
input:focus-within {
outline: none;
border-color: rgb(169, 227, 157);
border-width: 3px;
font-family: 'Staatliches', sans-serif;
}
textarea:focus-within {
outline: none;
border-color: rgb(169, 227, 157);
border-width: 3px;
}
#submit-button {
background-color: rgb(185, 201, 135);
border-radius: 20px;
border-color: #fff;
padding: 10px 18px;
text-align: center;
display: flex;
margin: auto;
margin-bottom: 20px;
}
button:hover {
opacity: 80%;
cursor: pointer;
}
</style>
-9
View File
@@ -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;
+40
View File
@@ -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 });
}
};
-78
View File
@@ -1,78 +0,0 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { user } from '$lib/stores/user.js';
import { type User } from '@supabase/auth-js';
export let data;
let { supabase } = data;
$: ({ supabase } = data);
let localUser: User | null;
const unsubscribe = user.subscribe((value) => {
localUser = value;
});
onDestroy(unsubscribe);
</script>
<div class="h-screen">
{#if localUser}
<div id="banner">
<div id="avatar">
<div
class="avatar mt-10 ml-20 w-14 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2"
>
<div class="rounded-full"><img src="/OIG5.jpg" alt="profile icon" /></div>
</div>
<button class="btn btn-outline btn-primary">Upload new avatar</button>
</div>
<div id="personal-information">
<p class="text-white font-mono text-3xl font-bold">{localUser?.email}</p>
<p class="text-white font-mono text-3xl font-bold">Title</p>
<p class="text-white font-mono text-3xl font-bold">Level</p>
</div>
</div>
<div id="profile-content">
<div id="pokemon-collection-summary">
<div class="radial-progress top-0 left-0" style="--value:70;" role="progressbar">70%</div>
<div class="caught-text top-20 left-30 font-mono text-xl">
<p>Total Pokemon Caught</p>
</div>
<h2 class="">Types Collected</h2>
<ul>
<li class="type-list-item font-mono">Fire</li>
</ul>
<br />
any other relevant statistics or achievements
</div>
<div id="customisation-options">
theme selection
<br />
profile background
<br />
any other features that enhance user experience and personalisation
</div>
<div id="social-sharing">
options for users to share their profile or achievements on social media platforms,
encouraging engagement and interaction with other users
</div>
</div>
{:else}
<p>Please log in to view this page</p>
{/if}
</div>
<style>
#banner {
background: rgb(41, 106, 170);
background: linear-gradient(0deg, rgba(41, 106, 170, 1) 0%, rgba(123, 225, 255, 1) 100%);
}
#profile-content {
background-color: rgb(255, 247, 238);
color: black;
}
.ring {
box-shadow: 0 0 0 4px rgb(149, 175, 230);
}
</style>
@@ -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;
$$;