mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-13 02:53:45 +00:00
feat(#60): Add stats and filters to pokedex page
This commit is contained in:
@@ -20,6 +20,87 @@
|
|||||||
export let createCatchRecords = () => {};
|
export let createCatchRecords = () => {};
|
||||||
export let onPokemonClick: (pokemon: CombinedData) => void = () => {};
|
export let onPokemonClick: (pokemon: CombinedData) => void = () => {};
|
||||||
|
|
||||||
|
let filterNotCaught = false;
|
||||||
|
let filterNeedsToEvolve = false;
|
||||||
|
let filterInHome = false;
|
||||||
|
let filterNotInHome = false;
|
||||||
|
|
||||||
|
let filteredCombinedData: CombinedData[] = [];
|
||||||
|
let filteredTotal = 0;
|
||||||
|
let overallTotal = 0;
|
||||||
|
let overallCaughtCount = 0;
|
||||||
|
let overallNotCaughtCount = 0;
|
||||||
|
let overallNeedsToEvolveCount = 0;
|
||||||
|
let overallInHomeCount = 0;
|
||||||
|
let overallNotInHomeCount = 0;
|
||||||
|
let filtersActive = false;
|
||||||
|
let filtersKey = '';
|
||||||
|
|
||||||
|
function normalizedStatus(catchRecord: CatchRecord | null) {
|
||||||
|
return {
|
||||||
|
caught: !!catchRecord?.caught,
|
||||||
|
needsToEvolve: !!catchRecord?.haveToEvolve,
|
||||||
|
inHome: !!catchRecord?.inHome
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilters(catchRecord: CatchRecord | null) {
|
||||||
|
const status = normalizedStatus(catchRecord);
|
||||||
|
if (!filtersActive) return true;
|
||||||
|
|
||||||
|
const isCaught = status.caught || status.needsToEvolve;
|
||||||
|
const isNotCaught = !isCaught;
|
||||||
|
const isNeedsToEvolve = status.needsToEvolve;
|
||||||
|
const isInHome = status.inHome;
|
||||||
|
const isNotInHome = !status.inHome;
|
||||||
|
|
||||||
|
// OR within each group, AND between groups.
|
||||||
|
const progressGroupActive = filterNotCaught || filterNeedsToEvolve;
|
||||||
|
const homeGroupActive = filterInHome || filterNotInHome;
|
||||||
|
|
||||||
|
const progressMatch =
|
||||||
|
!progressGroupActive ||
|
||||||
|
(filterNotCaught && isNotCaught) ||
|
||||||
|
(filterNeedsToEvolve && isNeedsToEvolve);
|
||||||
|
|
||||||
|
const homeMatch =
|
||||||
|
!homeGroupActive || (filterInHome && isInHome) || (filterNotInHome && isNotInHome);
|
||||||
|
|
||||||
|
return progressMatch && homeMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filtersActive = filterNotCaught || filterNeedsToEvolve || filterInHome || filterNotInHome;
|
||||||
|
$: filtersKey = `${filterNotCaught}-${filterNeedsToEvolve}-${filterInHome}-${filterNotInHome}`;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Ensure this recalculates when any filter changes (Svelte doesn't track function internals).
|
||||||
|
filtersKey;
|
||||||
|
filteredCombinedData = combinedData
|
||||||
|
? combinedData.filter(({ catchRecord }) => matchesFilters(catchRecord))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filteredTotal = filteredCombinedData.length;
|
||||||
|
$: overallTotal = combinedData?.length ?? 0;
|
||||||
|
|
||||||
|
$: overallCaughtCount = (combinedData ?? []).reduce((acc, { catchRecord }) => {
|
||||||
|
const status = normalizedStatus(catchRecord);
|
||||||
|
// Treat "needs to evolve" as a subset of "caught" (it is caught, just not finished).
|
||||||
|
return acc + (status.caught || status.needsToEvolve ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$: overallNotCaughtCount = overallTotal - overallCaughtCount;
|
||||||
|
|
||||||
|
$: overallNeedsToEvolveCount = (combinedData ?? []).reduce((acc, { catchRecord }) => {
|
||||||
|
return acc + (normalizedStatus(catchRecord).needsToEvolve ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$: overallInHomeCount = (combinedData ?? []).reduce((acc, { catchRecord }) => {
|
||||||
|
return acc + (normalizedStatus(catchRecord).inHome ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$: overallNotInHomeCount = overallTotal - overallInHomeCount;
|
||||||
|
|
||||||
const BOX_VIEW_LAYOUT_STORAGE_KEY = 'livingdex:boxViewLayout:v1';
|
const BOX_VIEW_LAYOUT_STORAGE_KEY = 'livingdex:boxViewLayout:v1';
|
||||||
type BoxViewLayout = 'comfortable' | 'compact' | 'ultra';
|
type BoxViewLayout = 'comfortable' | 'compact' | 'ultra';
|
||||||
let boxViewLayout: BoxViewLayout = 'comfortable';
|
let boxViewLayout: BoxViewLayout = 'comfortable';
|
||||||
@@ -89,86 +170,162 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onInHomeFilterChange(event: Event) {
|
||||||
|
const checked = (event.currentTarget as HTMLInputElement).checked;
|
||||||
|
if (checked) filterNotInHome = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNotInHomeFilterChange(event: Event) {
|
||||||
|
const checked = (event.currentTarget as HTMLInputElement).checked;
|
||||||
|
if (checked) filterInHome = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="flex-1 p-4 w-full">
|
<main class="flex-1 p-4 w-full">
|
||||||
<div class="max-w-fit mx-auto">
|
<div class="max-w-fit mx-auto">
|
||||||
{#if combinedData && combinedData.length > 0}
|
{#if combinedData && combinedData.length > 0}
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<div class="flex flex-col gap-3 mb-4">
|
<div class="card bg-base-100 shadow mb-4">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="card-body p-4 flex flex-col gap-3">
|
||||||
<label class="label p-0" for="box-view-layout">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<span class="label-text font-semibold">Box view layout</span>
|
<label class="label p-0" for="box-view-layout">
|
||||||
</label>
|
<span class="label-text font-semibold">Box view layout</span>
|
||||||
<select
|
</label>
|
||||||
id="box-view-layout"
|
<select
|
||||||
class="select select-bordered select-sm"
|
id="box-view-layout"
|
||||||
bind:value={boxViewLayout}
|
class="select select-bordered select-sm"
|
||||||
on:change={onBoxViewLayoutChange}
|
bind:value={boxViewLayout}
|
||||||
aria-label="Choose box view layout density"
|
on:change={onBoxViewLayoutChange}
|
||||||
>
|
aria-label="Choose box view layout density"
|
||||||
<option value="comfortable">Comfortable (2 boxes/row)</option>
|
>
|
||||||
<option value="compact">Compact (3 boxes/row)</option>
|
<option value="comfortable">Comfortable (2 boxes/row)</option>
|
||||||
<option value="ultra">Ultra (4 boxes/row)</option>
|
<option value="compact">Compact (3 boxes/row)</option>
|
||||||
</select>
|
<option value="ultra">Ultra (4 boxes/row)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||||
<span class="font-semibold">Legend:</span>
|
<span class="font-semibold">Legend:</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 rounded-full border border-green-700 bg-green-600 px-2 py-0.5 text-white"
|
class="inline-flex items-center gap-1 rounded-full border border-green-700 bg-green-600 px-2 py-0.5 text-white"
|
||||||
>
|
>
|
||||||
<span class="status-badge status-badge--caught" aria-hidden="true">
|
<span class="status-badge status-badge--caught" aria-hidden="true">
|
||||||
<svg
|
<svg
|
||||||
class="status-icon"
|
class="status-icon"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#ffffff"
|
stroke="#ffffff"
|
||||||
stroke-width="3"
|
stroke-width="3"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path d="M5 13l4 4L19 7" />
|
<path d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>Caught</span>
|
||||||
</span>
|
</span>
|
||||||
<span>Caught</span>
|
<span
|
||||||
</span>
|
class="inline-flex items-center gap-1 rounded-full border border-yellow-700 bg-yellow-500 px-2 py-0.5 text-white"
|
||||||
<span
|
>
|
||||||
class="inline-flex items-center gap-1 rounded-full border border-yellow-700 bg-yellow-500 px-2 py-0.5 text-white"
|
<span class="status-badge status-badge--evolve" aria-hidden="true">
|
||||||
>
|
<svg
|
||||||
<span class="status-badge status-badge--evolve" aria-hidden="true">
|
class="status-icon"
|
||||||
<svg
|
viewBox="0 0 24 24"
|
||||||
class="status-icon"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="#ffffff"
|
||||||
fill="none"
|
stroke-width="3"
|
||||||
stroke="#ffffff"
|
stroke-linecap="round"
|
||||||
stroke-width="3"
|
stroke-linejoin="round"
|
||||||
stroke-linecap="round"
|
>
|
||||||
stroke-linejoin="round"
|
<path d="M12 19V5" />
|
||||||
>
|
<path d="M5 12l7-7 7 7" />
|
||||||
<path d="M12 19V5" />
|
</svg>
|
||||||
<path d="M5 12l7-7 7 7" />
|
</span>
|
||||||
</svg>
|
<span>Caught but needs to evolve</span>
|
||||||
</span>
|
</span>
|
||||||
<span>Caught but needs to evolve</span>
|
<span
|
||||||
</span>
|
class="inline-flex items-center gap-1 rounded-full border border-sky-700 bg-sky-600 px-2 py-0.5 text-white"
|
||||||
<span
|
>
|
||||||
class="inline-flex items-center gap-1 rounded-full border border-sky-700 bg-sky-600 px-2 py-0.5 text-white"
|
<span class="status-badge status-badge--home" aria-hidden="true">
|
||||||
>
|
<svg
|
||||||
<span class="status-badge status-badge--home" aria-hidden="true">
|
class="status-icon"
|
||||||
<svg
|
viewBox="0 0 24 24"
|
||||||
class="status-icon"
|
fill="#ffffff"
|
||||||
viewBox="0 0 24 24"
|
stroke="#ffffff"
|
||||||
fill="#ffffff"
|
stroke-width="2"
|
||||||
stroke="#ffffff"
|
stroke-linecap="round"
|
||||||
stroke-width="2"
|
stroke-linejoin="round"
|
||||||
stroke-linecap="round"
|
>
|
||||||
stroke-linejoin="round"
|
<path d="M12 3 3 10.5V21a1 1 0 0 0 1 1h5v-6h6v6h5a1 1 0 0 0 1-1V10.5L12 3Z" />
|
||||||
>
|
</svg>
|
||||||
<path d="M12 3 3 10.5V21a1 1 0 0 0 1 1h5v-6h6v6h5a1 1 0 0 0 1-1V10.5L12 3Z" />
|
</span>
|
||||||
</svg>
|
<span>In Home</span>
|
||||||
</span>
|
</span>
|
||||||
<span>In Home</span>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<span class="font-semibold">Filters:</span>
|
||||||
|
<label class="label cursor-pointer gap-2 p-0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={filterNotCaught}
|
||||||
|
/>
|
||||||
|
<span class="label-text">Not caught</span>
|
||||||
|
</label>
|
||||||
|
<label class="label cursor-pointer gap-2 p-0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={filterNeedsToEvolve}
|
||||||
|
/>
|
||||||
|
<span class="label-text">Needs to evolve</span>
|
||||||
|
</label>
|
||||||
|
<label class="label cursor-pointer gap-2 p-0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={filterInHome}
|
||||||
|
on:change={onInHomeFilterChange}
|
||||||
|
/>
|
||||||
|
<span class="label-text">In HOME</span>
|
||||||
|
</label>
|
||||||
|
<label class="label cursor-pointer gap-2 p-0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={filterNotInHome}
|
||||||
|
on:change={onNotInHomeFilterChange}
|
||||||
|
/>
|
||||||
|
<span class="label-text">Not in HOME</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-between gap-2 rounded-box bg-base-200/60 px-3 py-2"
|
||||||
|
>
|
||||||
|
<div class="text-sm text-base-content/70">
|
||||||
|
<span class="font-semibold text-base-content">Caught</span>
|
||||||
|
{overallCaughtCount}
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span class="font-semibold text-base-content">Not caught</span>
|
||||||
|
{overallNotCaughtCount}
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span class="font-semibold text-base-content">Needs to evolve</span>
|
||||||
|
{overallNeedsToEvolveCount}
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span class="font-semibold text-base-content">In HOME</span>
|
||||||
|
{overallInHomeCount}
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span class="font-semibold text-base-content">Not in HOME</span>
|
||||||
|
{overallNotInHomeCount}
|
||||||
|
</div>
|
||||||
|
<div class="badge badge-outline">
|
||||||
|
Showing {filteredTotal} of {overallTotal}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,15 +355,20 @@
|
|||||||
<div class="grid grid-cols-6">
|
<div class="grid grid-cols-6">
|
||||||
{#each combinedData as { pokedexEntry, catchRecord }, index}
|
{#each combinedData as { pokedexEntry, catchRecord }, index}
|
||||||
{@const placement = calculateBoxPlacement(index)}
|
{@const placement = calculateBoxPlacement(index)}
|
||||||
|
{@const isFilteredOut =
|
||||||
|
filtersActive && !!filtersKey && !matchesFilters(catchRecord)}
|
||||||
{#if placement.box === boxNumber}
|
{#if placement.box === boxNumber}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="pokemon-box {cellStatusClasses(
|
class="pokemon-box {cellStatusClasses(catchRecord)} {isFilteredOut
|
||||||
catchRecord
|
? 'pokemon-box--filtered-out'
|
||||||
)} hover:scale-105 hover:shadow-lg hover:z-50 transition-all cursor-pointer relative"
|
: '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};
|
style="grid-column-start: {placement.column}; grid-row-start: {placement.row};
|
||||||
{cellBackgroundColourStyle(index, catchRecord)}"
|
{cellBackgroundColourStyle(index, catchRecord)}"
|
||||||
on:click={() => onPokemonClick({ pokedexEntry, catchRecord })}
|
aria-disabled={isFilteredOut}
|
||||||
|
on:click={() => {
|
||||||
|
if (!isFilteredOut) onPokemonClick({ pokedexEntry, catchRecord });
|
||||||
|
}}
|
||||||
aria-label="View details for {pokedexEntry.pokemon}. Status: {statusLabel(
|
aria-label="View details for {pokedexEntry.pokemon}. Status: {statusLabel(
|
||||||
catchRecord
|
catchRecord
|
||||||
)}"
|
)}"
|
||||||
@@ -350,6 +512,12 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pokemon-box--filtered-out {
|
||||||
|
opacity: 0.25;
|
||||||
|
filter: grayscale(1);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.pokemon-box-inner {
|
.pokemon-box-inner {
|
||||||
padding: var(--cell-padding, 1rem);
|
padding: var(--cell-padding, 1rem);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user