From 200a6e8b61ef9bed8a40c222aeda412252bcf76c Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sun, 28 Jun 2026 15:45:57 -0700 Subject: [PATCH] feat: add search and filters to the Tracked Domains list (#39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add search and filters to the Tracked Domains list Client-side search + device/country filtering over the tracked-domains set, handling multi-config domains. Closes #33. * design: gate tracked-domains filter bar by volume and flatten its chrome Hide the domain filter bar until there are enough rows to warrant it (>= 6, or whenever a filter is active so it can't be orphaned), drop the redundant 'Refine results' sub-header in favor of an inline clear control, and remove the gradient band so the filter row shares the card surface with a single divider under the header. * review: fix failing test, drop dead exports, clear oxlint/knip errors - Align getDomainListFilterOptions test fixtures with the real LOCATIONS map (short labels FR/UK), which the suite asserted as full names — the list rows already render short labels, so the dropdown matches them. - Inline the single-use FilterPanelHeader back into FilterPanel (it was extracted to share with the domain filter bar, which no longer uses it). - Validate the device + onChange({ ...filters, query: event.target.value }) + } + /> + + + + {activeFilterCount > 0 && ( + + )} + + + ); +} + function RangeFilter({ title, minValue, @@ -135,6 +263,57 @@ function RangeFilter({ ); } +export function applyDomainListFilters( + configs: T[], + filters: DomainListFilters, +): T[] { + const query = filters.query.trim().toLowerCase(); + const locationCode = + filters.locationCode === "all" ? null : Number(filters.locationCode); + + return configs.filter((config) => { + if (query && !config.domain.toLowerCase().includes(query)) return false; + + if (filters.device !== "all" && config.devices !== filters.device) { + return false; + } + + if (locationCode !== null && config.locationCode !== locationCode) { + return false; + } + + return true; + }); +} + +export function getDomainListFilterOptions(configs: DomainFilterableConfig[]): { + devices: DomainListFilterOption[]; + locations: DomainListFilterOption[]; +} { + const deviceValues = new Set(configs.map((config) => config.devices)); + const devices = DEVICE_FILTER_ORDER.filter((device) => + deviceValues.has(device), + ).map((device) => ({ + value: device, + label: devicesLabel(device), + })); + + const locationMap = new Map(); + for (const config of configs) { + locationMap.set( + config.locationCode, + LOCATIONS[config.locationCode] ?? String(config.locationCode), + ); + } + + const locations = Array.from(locationMap, ([code, label]) => ({ + value: String(code), + label, + })).toSorted((a, b) => a.label.localeCompare(b.label)); + + return { devices, locations }; +} + export function applyFilters( rows: RankTrackingRow[], filters: Filters, @@ -208,3 +387,13 @@ export function countActiveFilters(filters: Filters): number { if (filters.minMobilePos || filters.maxMobilePos) count++; return count; } + +export function countActiveDomainListFilters( + filters: DomainListFilters, +): number { + let count = 0; + if (filters.query.trim()) count++; + if (filters.device !== "all") count++; + if (filters.locationCode !== "all") count++; + return count; +}