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) -![Version](https://img.shields.io/badge/version-v1.27.3-e3000b?style=flat-square) ![Dark Theme](https://img.shields.io/badge/theme-dark-1a1a2e?style=flat-square) ![TCGdex](https://img.shields.io/badge/card%20data-TCGdex-e3000b?style=flat-square) ![Docker](https://img.shields.io/badge/deploy-Docker-2496ed?style=flat-square) ![FastAPI](https://img.shields.io/badge/backend-FastAPI-009688?style=flat-square) ![React](https://img.shields.io/badge/frontend-React%2018-61dafb?style=flat-square) [![Ko-fi](https://img.shields.io/badge/support-Ko--fi-ff5e5b?style=flat-square&logo=ko-fi&logoColor=white)](https://ko-fi.com/gillesromer) +![Version](https://img.shields.io/badge/version-v1.27.4-e3000b?style=flat-square) ![Dark Theme](https://img.shields.io/badge/theme-dark-1a1a2e?style=flat-square) ![TCGdex](https://img.shields.io/badge/card%20data-TCGdex-e3000b?style=flat-square) ![Docker](https://img.shields.io/badge/deploy-Docker-2496ed?style=flat-square) ![FastAPI](https://img.shields.io/badge/backend-FastAPI-009688?style=flat-square) ![React](https://img.shields.io/badge/frontend-React%2018-61dafb?style=flat-square) [![Ko-fi](https://img.shields.io/badge/support-Ko--fi-ff5e5b?style=flat-square&logo=ko-fi&logoColor=white)](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). ![WebApp Preview](preview-homescreen.png) 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() + }) })