From cc6ee328e537cefcf7d3d986f1d824ff084a0bf0 Mon Sep 17 00:00:00 2001
From: Benjamin Engel <9927594+engelben@users.noreply.github.com>
Date: Mon, 27 Jul 2026 15:43:38 +0200
Subject: [PATCH 1/2] =?UTF-8?q?Fix=20mobile=20Pok=C3=A9dex=20region=20filt?=
=?UTF-8?q?ers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/src/i18n/de.js | 1 +
frontend/src/i18n/en.js | 1 +
frontend/src/pages/Pokedex.jsx | 23 ++++++++++++++++++++---
3 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/frontend/src/i18n/de.js b/frontend/src/i18n/de.js
index ff841819..ca49d956 100644
--- a/frontend/src/i18n/de.js
+++ b/frontend/src/i18n/de.js
@@ -26,6 +26,7 @@ const de = {
subtitle: 'Eine beliebige passende Karte vervollständigt das Pokémon.',
searchPlaceholder: 'Name oder Pokédex-Nummer',
national: 'National',
+ generationFilter: 'Pokédex-Region',
generation: 'Gen {generation}',
all: 'Alle',
owned: 'Vorhanden',
diff --git a/frontend/src/i18n/en.js b/frontend/src/i18n/en.js
index b84ea0d3..199a5a21 100644
--- a/frontend/src/i18n/en.js
+++ b/frontend/src/i18n/en.js
@@ -26,6 +26,7 @@ const en = {
subtitle: 'Own any matching card to complete a species.',
searchPlaceholder: 'Name or Pokédex number',
national: 'National',
+ generationFilter: 'Pokédex region',
generation: 'Gen {generation}',
all: 'All',
owned: 'Owned',
diff --git a/frontend/src/pages/Pokedex.jsx b/frontend/src/pages/Pokedex.jsx
index b040921a..95e2c552 100644
--- a/frontend/src/pages/Pokedex.jsx
+++ b/frontend/src/pages/Pokedex.jsx
@@ -170,11 +170,28 @@ export default function Pokedex() {
/>
-
+
+
+
@@ -183,7 +200,7 @@ export default function Pokedex() {
type="button"
key={item.id}
onClick={() => selectGeneration(item.id)}
- className={clsx('whitespace-nowrap rounded-full border px-3 py-1.5 text-xs font-bold', generation === item.id ? 'border-brand-red bg-brand-red/20 text-brand-red' : 'border-border text-text-secondary')}
+ className={clsx('shrink-0 whitespace-nowrap rounded-full border px-3 py-1.5 text-xs font-bold', generation === item.id ? 'border-brand-red bg-brand-red/20 text-brand-red' : 'border-border text-text-secondary')}
>
Gen {item.id} · {item.region}
From 48d6abf4beff511cd7e5d8c417dda434d25d4dee Mon Sep 17 00:00:00 2001
From: Git-Romer
Date: Tue, 28 Jul 2026 15:57:08 +0200
Subject: [PATCH 2/2] =?UTF-8?q?fix:=20harden=20mobile=20Pok=C3=A9dex=20gen?=
=?UTF-8?q?eration=20filter?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 4 +-
VERSION | 2 +-
frontend/package-lock.json | 4 +-
frontend/package.json | 2 +-
.../components/PokedexGenerationFilter.jsx | 76 ++++++++++++++++++
.../PokedexGenerationFilter.test.js | 53 +++++++++++++
frontend/src/pages/Pokedex.jsx | 78 ++++++-------------
frontend/src/utils/pokedexUrlState.js | 22 ++++++
frontend/src/utils/pokedexUrlState.test.js | 27 ++++++-
9 files changed, 206 insertions(+), 62 deletions(-)
create mode 100644 frontend/src/components/PokedexGenerationFilter.jsx
create mode 100644 frontend/src/components/PokedexGenerationFilter.test.js
diff --git a/README.md b/README.md
index e18e6209..83c3b5fb 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,9 @@ Be kind. Be clear. Assume good intent. Keep feedback constructive.
- 👤 **Creator:** [Gilles Romer](https://romerg.de/)
- ✉️ **Contact:** [info@romerg.de](mailto:info@romerg.de)
-      [](https://ko-fi.com/gillesromer)
+      [](https://ko-fi.com/gillesromer)
-**Current version:** `v1.27.3` · Releases are tracked on the [GitHub Releases page](https://github.com/Git-Romer/pokecollector/releases).
+**Current version:** `v1.27.4` · Releases are tracked on the [GitHub Releases page](https://github.com/Git-Romer/pokecollector/releases).

diff --git a/VERSION b/VERSION
index 3bae5204..d6201580 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.27.3
+1.27.4
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 08fc5f53..ae58e99d 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "pokemon-tcg-collection",
- "version": "1.27.3",
+ "version": "1.27.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pokemon-tcg-collection",
- "version": "1.27.3",
+ "version": "1.27.4",
"dependencies": {
"@tanstack/react-query": "^5.18.0",
"axios": "^1.18.0",
diff --git a/frontend/package.json b/frontend/package.json
index b9406ece..80720a41 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "pokemon-tcg-collection",
"private": true,
- "version": "1.27.3",
+ "version": "1.27.4",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/PokedexGenerationFilter.jsx b/frontend/src/components/PokedexGenerationFilter.jsx
new file mode 100644
index 00000000..2db66654
--- /dev/null
+++ b/frontend/src/components/PokedexGenerationFilter.jsx
@@ -0,0 +1,76 @@
+import clsx from 'clsx'
+
+export const POKEDEX_GENERATIONS = [
+ { id: 1, region: 'Kanto', range: '#001–151' },
+ { id: 2, region: 'Johto', range: '#152–251' },
+ { id: 3, region: 'Hoenn', range: '#252–386' },
+ { id: 4, region: 'Sinnoh', range: '#387–493' },
+ { id: 5, region: 'Unova', range: '#494–649' },
+ { id: 6, region: 'Kalos', range: '#650–721' },
+ { id: 7, region: 'Alola', range: '#722–809' },
+ { id: 8, region: 'Galar', range: '#810–905' },
+ { id: 9, region: 'Paldea', range: '#906–1025' },
+]
+
+export default function PokedexGenerationFilter({ generation, onSelectGeneration, t }) {
+ const label = t('pokedex.generationFilter')
+
+ return (
+ <>
+
+
+
+
+ {POKEDEX_GENERATIONS.map((item) => (
+
+ ))}
+
+ >
+ )
+}
diff --git a/frontend/src/components/PokedexGenerationFilter.test.js b/frontend/src/components/PokedexGenerationFilter.test.js
new file mode 100644
index 00000000..d223ed6e
--- /dev/null
+++ b/frontend/src/components/PokedexGenerationFilter.test.js
@@ -0,0 +1,53 @@
+import { createElement } from 'react'
+import { renderToStaticMarkup } from 'react-dom/server'
+import { describe, expect, it, vi } from 'vitest'
+import PokedexGenerationFilter from './PokedexGenerationFilter'
+
+const translations = {
+ 'pokedex.generationFilter': 'Pokédex region',
+ 'pokedex.national': 'National',
+}
+const t = key => translations[key] || key
+
+describe('PokedexGenerationFilter', () => {
+ it('renders a labelled mobile select and a non-shrinking desktop control', () => {
+ const markup = renderToStaticMarkup(createElement(PokedexGenerationFilter, {
+ generation: 3,
+ onSelectGeneration: vi.fn(),
+ t,
+ }))
+
+ expect(markup).toContain('
-
-
-
-
- {GENERATIONS.map((item) => (
-
- ))}
-
+
{['all', 'owned', 'missing'].map((value) => (
@@ -243,7 +211,7 @@ export default function Pokedex() {
{!isLoading && !isError && entries.length === 0 &&
{t('common.noResults')}
}
{!isLoading && grouped.map((group) => {
- const info = GENERATIONS.find((item) => item.id === group.generation)
+ const info = POKEDEX_GENERATIONS.find((item) => item.id === group.generation)
return (
{!generation && !search.trim() && info && (
diff --git a/frontend/src/utils/pokedexUrlState.js b/frontend/src/utils/pokedexUrlState.js
index 53c17524..26555d43 100644
--- a/frontend/src/utils/pokedexUrlState.js
+++ b/frontend/src/utils/pokedexUrlState.js
@@ -4,3 +4,25 @@ export const getPokedexGeneration = (searchParams) => {
? requestedGeneration
: null
}
+
+export const normalizePokedexSearchParams = (searchParams) => {
+ const normalized = new URLSearchParams(searchParams)
+ const requestedGenerations = normalized.getAll('generation')
+ if (!requestedGenerations.length) return normalized
+
+ const generation = getPokedexGeneration(normalized)
+ if (generation) normalized.set('generation', String(generation))
+ else normalized.delete('generation')
+
+ return normalized
+}
+
+export const setPokedexGeneration = (searchParams, generation) => {
+ const updated = new URLSearchParams(searchParams)
+ if (Number.isInteger(generation) && generation >= 1 && generation <= 9) {
+ updated.set('generation', String(generation))
+ } else {
+ updated.delete('generation')
+ }
+ return updated
+}
diff --git a/frontend/src/utils/pokedexUrlState.test.js b/frontend/src/utils/pokedexUrlState.test.js
index cd36d874..eaeb401e 100644
--- a/frontend/src/utils/pokedexUrlState.test.js
+++ b/frontend/src/utils/pokedexUrlState.test.js
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
-import { getPokedexGeneration } from './pokedexUrlState'
+import {
+ getPokedexGeneration,
+ normalizePokedexSearchParams,
+ setPokedexGeneration,
+} from './pokedexUrlState'
describe('Pokédex URL state', () => {
it('follows generation changes from browser history', () => {
@@ -17,4 +21,25 @@ describe('Pokédex URL state', () => {
expect(getPokedexGeneration(new URLSearchParams('generation=10'))).toBeNull()
expect(getPokedexGeneration(new URLSearchParams('generation=2x'))).toBeNull()
})
+
+ it('normalizes malformed and duplicate generation parameters without removing unrelated state', () => {
+ expect(normalizePokedexSearchParams(
+ new URLSearchParams('generation=invalid&status=owned')
+ ).toString()).toBe('status=owned')
+ expect(normalizePokedexSearchParams(
+ new URLSearchParams('generation=02&generation=3&status=owned')
+ ).toString()).toBe('generation=2&status=owned')
+ })
+
+ it('writes valid generation changes and preserves unrelated search parameters', () => {
+ const initial = new URLSearchParams('status=owned&search=pika')
+
+ const johto = setPokedexGeneration(initial, 2)
+ expect(johto.toString()).toBe('status=owned&search=pika&generation=2')
+ expect(getPokedexGeneration(johto)).toBe(2)
+
+ const national = setPokedexGeneration(johto, null)
+ expect(national.toString()).toBe('status=owned&search=pika')
+ expect(getPokedexGeneration(national)).toBeNull()
+ })
})