add catchemall progress bars and hide uncatchable pokemon by default
All checks were successful
Test and deploy staging / staging (push) Successful in 28s

This commit is contained in:
2026-09-23 04:59:22 +00:00
parent eca43c82d0
commit f1cd0ef849
3 changed files with 193 additions and 19 deletions

View File

@@ -6,6 +6,7 @@
import type { PageProps } from './$types';
import type { Game } from '$lib/server/db/schema';
import ProgressBar from '$lib/components/ProgressBar.svelte';
import { summarizeProgress } from '$lib/catch-em-all/progress';
type Entry = { id: number; availability: Availability; note: string | null };
let { data, form }: PageProps = $props();
@@ -59,7 +60,6 @@
const visibleGames = $derived(
data.gamesConfigured ? data.games.filter((game) => selectedGameIds.has(game.id)) : data.games
);
const visibleGameIds = $derived(new Set(visibleGames.map((game) => game.id)));
let viewGameId = $state<number | null>(null);
$effect(() => {
if (!visibleGames.some((game) => game.id === viewGameId))
@@ -67,26 +67,48 @@
});
const viewGame = $derived(visibleGames.find((game) => game.id === viewGameId));
const catchableEntries = $derived(
data.pokemon
.flatMap((pokemon) => pokemon.entries)
.filter(
([, gameId, code]) => availabilityValues[code] === 'catchable' && visibleGameIds.has(gameId)
)
);
const total = $derived(catchableEntries.length);
const caughtCount = $derived(
catchableEntries.reduce((n, [id]) => n + (caught.has(id) ? 1 : 0), 0)
);
const percentage = $derived(total > 0 ? ((caughtCount / total) * 100).toFixed(1) : '0.0');
const visibleGameIds = $derived(new Set(visibleGames.map((game) => game.id)));
const progress = $derived(summarizeProgress(data.pokemon, visibleGameIds, caught));
const progressBars = $derived([
{
label: 'seen at least once',
value: progress.seenPokemon,
total: progress.catchablePokemon,
unit: 'Pokémon'
},
{
label: 'seen in every catchable game',
value: progress.completedPokemon,
total: progress.catchablePokemon,
unit: 'Pokémon'
},
{
label: 'games completed',
value: progress.completedGames,
total: progress.catchableGames,
unit: 'games'
}
]);
function percent(value: number, total: number) {
return total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
}
let search = $state('');
let showNonCatchable = $state(false);
let pageNumber = $state(1);
const pageSize = 25;
const filteredPokemon = $derived(
data.pokemon.filter((pokemon) =>
`${pokemon.number} ${pokemon.name}`.toLowerCase().includes(search.trim().toLowerCase())
)
data.pokemon.filter((pokemon) => {
if (
!showNonCatchable &&
viewGameId !== null &&
entryMap.get(pokemon.id)?.get(viewGameId)?.availability !== 'catchable'
)
return false;
return `${pokemon.number} ${pokemon.name}`
.toLowerCase()
.includes(search.trim().toLowerCase());
})
);
const pageCount = $derived(Math.max(1, Math.ceil(filteredPokemon.length / pageSize)));
$effect(() => {
@@ -151,12 +173,40 @@
<div class="mt-8 border-y border-[var(--site-border)] py-6">
<div class="flex flex-wrap items-baseline justify-between gap-2">
<p class="font-medium">overall progress</p>
<p class="text-2xl font-semibold tabular-nums">{percentage}%</p>
<p class="text-2xl font-semibold tabular-nums">
{percent(progress.caughtEntries, progress.catchableEntries)}%
</p>
</div>
<div class="mt-3">
<ProgressBar value={caughtCount} max={total} label="Overall catch progress" />
<ProgressBar
value={progress.caughtEntries}
max={progress.catchableEntries}
label="Overall catch progress"
/>
</div>
<p class="mt-2 text-base text-[var(--site-muted)]">{caughtCount} of {total} entries caught</p>
<p class="mt-2 text-base text-[var(--site-muted)]">
{progress.caughtEntries} of {progress.catchableEntries} catchable entries caught
</p>
<div class="mt-8 grid gap-x-10 gap-y-7 lg:grid-cols-3">
{#each progressBars as item (item.label)}
<div class="min-w-0">
<div class="flex flex-wrap items-baseline justify-between gap-2">
<p class="font-medium">{item.label}</p>
<p class="text-xl font-semibold tabular-nums">{percent(item.value, item.total)}%</p>
</div>
<div class="mt-3">
<ProgressBar value={item.value} max={item.total} label={item.label} small />
</div>
<p class="mt-2 text-base text-[var(--site-muted)]">
{item.value} of {item.total}
{item.unit}
</p>
</div>
{/each}
</div>
<p class="mt-6 text-base text-[var(--site-muted)]">
counts use your selected games and confirmed catchable entries.
</p>
</div>
<div class="mt-8 grid gap-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
@@ -174,6 +224,7 @@
>game
<select
bind:value={viewGameId}
onchange={() => (pageNumber = 1)}
disabled={!visibleGames.length}
class="mt-2 block w-full rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-3"
>
@@ -181,6 +232,18 @@
</select>
</label>
</div>
<label class="mt-5 flex cursor-pointer items-center gap-3">
<input
type="checkbox"
checked={showNonCatchable}
onchange={(event) => {
showNonCatchable = event.currentTarget.checked;
pageNumber = 1;
}}
class="h-5 w-5 accent-[var(--site-text)]"
/>
show non-catchable Pokémon
</label>
{#if viewGame}
<div class="mt-8">