From 8988bf3e9254933396cf1266145166c8078eeedb Mon Sep 17 00:00:00 2001 From: Asgeir Jacobsen Date: Thu, 5 Nov 2026 10:41:59 +0100 Subject: [PATCH 1/3] Fix: type dates in the search filters without jumps Typing in the From and To fields made them jump or clear. While a date is half typed the browser reports an empty value, and a year like "0002" counts as a valid date. Both went into the URL state and back into the controlled input, which rewrote the field. Add a DateInput that is uncontrolled and only passes a complete date (four-digit year) or a real clear on to the filters. It still syncs when a filter chip or "Clear filters" changes the value. Closes #139 --- frontend/src/components/SearchFilters.tsx | 60 ++++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/SearchFilters.tsx b/frontend/src/components/SearchFilters.tsx index a72e7c1..108ed46 100644 --- a/frontend/src/components/SearchFilters.tsx +++ b/frontend/src/components/SearchFilters.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@apollo/client/react' +import { useEffect, useRef, type ChangeEvent } from 'react' import { FILTER_OPTIONS_QUERY } from '../graphql/queries' import { createRetry } from '../utils/create-retry' import './SearchFilters.css' @@ -13,6 +14,55 @@ function countryName(code: string) { } } +interface DateInputProps { + id: string + value: string + min?: string + max?: string + onCommit: (value: string) => void +} + +/** + * A date field that tolerates typing. While the user types, the browser + * reports "" for an incomplete date and a valid date for a year like + * "0002". Feeding either back into a controlled input wipes or rewrites + * what is being typed, so the input is uncontrolled and only complete dates + * (four-digit year) or a real clear reach the URL state. + */ +function DateInput({ id, value, min, max, onCommit }: DateInputProps) { + const ref = useRef(null) + + // Keeps the field in step with changes made elsewhere, such as removing + // the filter chip, without touching it while the user is mid-edit. + useEffect(() => { + const input = ref.current + if (input && input.value !== value && !input.validity.badInput) { + input.value = value + } + }, [value]) + + function handleChange(event: ChangeEvent) { + const { value: next, validity } = event.currentTarget + if (next === '') { + if (!validity.badInput) onCommit('') + return + } + if (Number(next.slice(0, 4)) >= 1000) onCommit(next) + } + + return ( + + ) +} + export interface SearchFiltersValue { shape: string country: string @@ -105,22 +155,20 @@ export default function SearchFilters({
- onChange('from', event.target.value)} + onCommit={(value) => onChange('from', value)} />
- onChange('to', event.target.value)} + onCommit={(value) => onChange('to', value)} />
From 1055f9c1b6821ba7543a3df9162c37db37665856 Mon Sep 17 00:00:00 2001 From: Asgeir Jacobsen Date: Thu, 5 Nov 2026 10:45:02 +0100 Subject: [PATCH 2/3] Fix: make search and filter boxes fully clickable Only the row with the value reacted to clicks. The padding around it did nothing, and the label only focused the field without opening the dropdown or date picker. Stretch the field over the whole box and move the padding onto it, with the label in the same grid cell underneath. Add a subtle background tint and a pointer cursor on hover, so it is clear the whole box can be clicked. The tint also works on wide screens, where the joined bar keeps the border color. Closes #140 --- frontend/src/components/SearchBar.css | 36 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/SearchBar.css b/frontend/src/components/SearchBar.css index 8ff6944..fab2c5f 100644 --- a/frontend/src/components/SearchBar.css +++ b/frontend/src/components/SearchBar.css @@ -8,16 +8,26 @@ /* The label sits inside the box with its field, so the two read as one control. On wide screens the boxes join into one bar (SearchPage.css). */ .search-bar__field { - display: flex; - flex-direction: column; - padding: 8px 14px 6px; + /* The field is stretched over the whole box (both grid rows) and given + the padding itself, so a click anywhere in the box, also on the label + or the edge, lands on the field and opens it. The label sits in the + same cell underneath, in the space the field's top padding leaves. */ + --label-size: 13px; + --label-line: 1.3; + display: grid; + grid-template-rows: auto 36px; border: 1px solid var(--border); border-radius: 12px; background: var(--code-bg); - transition: border-color 0.15s; + transition: + border-color 0.15s, + background-color 0.15s; + /* The tint also shows on wide screens, where the joined bar keeps the + border color (SearchPage.css) and the border alone wouldn't change. */ &:hover { border-color: var(--accent-border); + background: color-mix(in srgb, var(--accent-bg) 60%, var(--code-bg)); } /* The box shows focus for the field inside it, also when the mouse @@ -28,18 +38,21 @@ } label { - font-size: 13px; + grid-area: 1 / 1; + padding: 8px 14px 0; + font-size: var(--label-size); font-weight: 600; - line-height: 1.3; + line-height: var(--label-line); color: var(--text); } input, select { + grid-area: 1 / 1 / 3 / 2; width: 100%; - height: 30px; + min-width: 0; box-sizing: border-box; - padding: 0; + padding: calc(8px + var(--label-size) * var(--label-line)) 14px 6px; border: 0; font: inherit; font-size: 16px; @@ -48,6 +61,13 @@ outline: none; } + /* Selects and date fields open a list or picker, so they show it is + clickable. The text search keeps its text cursor. */ + select, + input[type='date'] { + cursor: pointer; + } + input::placeholder { color: var(--text); opacity: 0.8; From db0af74795eb2bd369ef3542770615a42b451d9f Mon Sep 17 00:00:00 2001 From: Asgeir Jacobsen Date: Thu, 5 Nov 2026 12:34:05 +0100 Subject: [PATCH 3/3] Fix: Remove unused country name helper The merge from main left countryName unused, since the filters now use formatCountry. Remove it so lint passes. --- frontend/src/components/SearchFilters.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/frontend/src/components/SearchFilters.tsx b/frontend/src/components/SearchFilters.tsx index 34ad0aa..d1e1947 100644 --- a/frontend/src/components/SearchFilters.tsx +++ b/frontend/src/components/SearchFilters.tsx @@ -5,16 +5,6 @@ import { createRetry } from '../utils/create-retry' import { formatCountry } from '../utils/format-sighting' import './SearchFilters.css' -const countryNames = new Intl.DisplayNames(['en'], { type: 'region' }) - -function countryName(code: string) { - try { - return countryNames.of(code.toUpperCase()) ?? code - } catch { - return code - } -} - interface DateInputProps { id: string value: string