From 79001011dfa6a0a3d006fc898de950163fcb3f19 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sat, 18 Apr 2026 05:32:28 +0200 Subject: [PATCH] fix(search): support PLZ filter on home page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The home page dropped `plz` server-side, so /markets was called with radius but no center (unfiltered) and the PLZ input rendered empty after reload. +page.server.ts now reads plz, geocodes via /geocode, and echoes plz back in searchParams for form rehydration. Relaxes /geocode DTO + guard to accept PLZ without city — Nominatim already supports postal-only lookups. URL lat/lon (GPS flow) take priority over plz on tie-break; geocode failures fall through to no geo-filter so the page always renders. --- backend/internal/domain/market/dto.go | 2 +- backend/internal/pkg/geocode/nominatim.go | 4 +- web/src/routes/+page.server.ts | 66 +++++++++++++++-------- web/src/routes/+page.svelte | 1 + 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/backend/internal/domain/market/dto.go b/backend/internal/domain/market/dto.go index a5c197f..4a444d8 100644 --- a/backend/internal/domain/market/dto.go +++ b/backend/internal/domain/market/dto.go @@ -395,7 +395,7 @@ type SubmitMarketRequest struct { type GeocodeRequest struct { Street string `json:"street"` - City string `json:"city" validate:"required"` + City string `json:"city"` Zip string `json:"zip"` Country string `json:"country"` } diff --git a/backend/internal/pkg/geocode/nominatim.go b/backend/internal/pkg/geocode/nominatim.go index f4e4f84..479a09e 100644 --- a/backend/internal/pkg/geocode/nominatim.go +++ b/backend/internal/pkg/geocode/nominatim.go @@ -28,8 +28,8 @@ type nominatimResult struct { } func (g *Geocoder) Geocode(ctx context.Context, street, city, zip, country string) (*float64, *float64, error) { - if city == "" { - return nil, nil, fmt.Errorf("city is required for geocoding") + if city == "" && zip == "" { + return nil, nil, fmt.Errorf("city or zip is required for geocoding") } // Respect Nominatim rate limit: max 1 req/sec diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.server.ts index 53afc61..4270537 100644 --- a/web/src/routes/+page.server.ts +++ b/web/src/routes/+page.server.ts @@ -2,10 +2,34 @@ import type { PageServerLoad } from './$types.js'; import { apiFetch, buildSearchQuery } from '$lib/api/client.js'; import type { MarketSummary, PaginationMeta } from '$lib/api/types.js'; -export const load: PageServerLoad = async ({ url, fetch }) => { - const params: Record = {}; +type Coords = { lat?: string; lon?: string }; +async function resolveCoords( + plz: string | null, + urlLat: string | null, + urlLon: string | null, + fetch: typeof globalThis.fetch +): Promise { + if (urlLat && urlLon) return { lat: urlLat, lon: urlLon }; + if (!plz) return {}; + + try { + const res = await apiFetch<{ latitude: number | null; longitude: number | null }>('/geocode', { + method: 'POST', + body: JSON.stringify({ city: '', zip: plz, country: 'DE' }), + fetch + }); + const { latitude, longitude } = res.data; + if (latitude == null || longitude == null) return {}; + return { lat: String(latitude), lon: String(longitude) }; + } catch { + return {}; + } +} + +export const load: PageServerLoad = async ({ url, fetch }) => { const q = url.searchParams.get('q'); + const plz = url.searchParams.get('plz'); const lat = url.searchParams.get('lat'); const lon = url.searchParams.get('lon'); const radius = url.searchParams.get('radius'); @@ -14,9 +38,12 @@ export const load: PageServerLoad = async ({ url, fetch }) => { const sort = url.searchParams.get('sort'); const page = url.searchParams.get('page'); + const coords = await resolveCoords(plz, lat, lon, fetch); + + const params: Record = {}; if (q) params.q = q; - if (lat) params.lat = lat; - if (lon) params.lon = lon; + if (coords.lat) params.lat = coords.lat; + if (coords.lon) params.lon = coords.lon; if (radius) params.radius = radius; if (from) params.from = from; if (to) params.to = to; @@ -26,34 +53,29 @@ export const load: PageServerLoad = async ({ url, fetch }) => { const query = buildSearchQuery(params); const path = `/markets${query ? `?${query}` : ''}`; + const searchParams = { + q: q ?? '', + plz: plz ?? '', + lat: lat ? Number(lat) : undefined, + lon: lon ? Number(lon) : undefined, + radius: radius ? Number(radius) : 25, + from: from ?? '', + to: to ?? '', + sort: sort ?? '' + }; + try { const res = await apiFetch(path, { fetch }); return { markets: res.data, meta: res.meta as PaginationMeta, - searchParams: { - q: q ?? '', - lat: lat ? Number(lat) : undefined, - lon: lon ? Number(lon) : undefined, - radius: radius ? Number(radius) : 25, - from: from ?? '', - to: to ?? '', - sort: sort ?? '' - } + searchParams }; } catch { return { markets: [] as MarketSummary[], meta: { page: 1, per_page: 20, total: 0, total_pages: 0 } as PaginationMeta, - searchParams: { - q: q ?? '', - lat: lat ? Number(lat) : undefined, - lon: lon ? Number(lon) : undefined, - radius: radius ? Number(radius) : 25, - from: from ?? '', - to: to ?? '', - sort: sort ?? '' - } + searchParams }; } }; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 40d04d4..8cf279f 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -50,6 +50,7 @@