From 5337214d18bcd211fa8e8d051d3502950a230066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Mon, 2 Mar 2026 22:05:37 +0100 Subject: [PATCH] sort constituency representatives matching user's city to top, show "Dein Wahlkreis" badge Co-Authored-By: Claude Opus 4.6 --- .../components/bundestag-configure.tsx | 17 +++++++--- .../landtag/components/landtag-configure.tsx | 8 +++-- src/features/location/lib/geo.ts | 17 +++++++++- src/shared/components/representative-list.tsx | 32 +++++++++++++++---- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/features/bundestag/components/bundestag-configure.tsx b/src/features/bundestag/components/bundestag-configure.tsx index f135b74..6edb879 100644 --- a/src/features/bundestag/components/bundestag-configure.tsx +++ b/src/features/bundestag/components/bundestag-configure.tsx @@ -2,7 +2,7 @@ import { RepresentativeList } from "@/shared/components/representative-list" import { useDb } from "@/shared/db/provider" import type { MandateWithPolitician } from "@/shared/lib/aw-api" import { useEffect, useState } from "react" -import { fetchAndCacheBundestagMandates } from "../../location/lib/geo" +import { fetchAndCacheBundestagMandates, loadCachedResult } from "../../location/lib/geo" import { useBundestagUI } from "../store" export function BundestagConfigure() { @@ -11,12 +11,16 @@ export function BundestagConfigure() { const setPoliticianSearch = useBundestagUI((s) => s.setPoliticianSearch) const [mandates, setMandates] = useState([]) + const [userCity, setUserCity] = useState(null) const [loadingMandates, setLoadingMandates] = useState(false) useEffect(() => { setLoadingMandates(true) - fetchAndCacheBundestagMandates(db) - .then(setMandates) + Promise.all([fetchAndCacheBundestagMandates(db), loadCachedResult(db)]) + .then(([m, geo]) => { + setMandates(m) + if (geo?.userCity) setUserCity(geo.userCity) + }) .finally(() => setLoadingMandates(false)) }, [db]) @@ -27,7 +31,12 @@ export function BundestagConfigure() {
) : mandates.length > 0 ? ( - + ) : (

Keine Abgeordneten verfügbar.

)} diff --git a/src/features/landtag/components/landtag-configure.tsx b/src/features/landtag/components/landtag-configure.tsx index 2e78885..9777afd 100644 --- a/src/features/landtag/components/landtag-configure.tsx +++ b/src/features/landtag/components/landtag-configure.tsx @@ -11,11 +11,15 @@ export function LandtagConfigure() { const search = useLandtagUI((s) => s.politicianSearch) const setSearch = useLandtagUI((s) => s.setPoliticianSearch) const [mandates, setMandates] = useState([]) + const [userCity, setUserCity] = useState(null) const [loaded, setLoaded] = useState(false) useEffect(() => { loadCachedResult(db).then((cached) => { - if (cached) setMandates(cached.mandates) + if (cached) { + setMandates(cached.mandates) + setUserCity(cached.userCity) + } setLoaded(true) }) }, [db]) @@ -40,7 +44,7 @@ export function LandtagConfigure() {
) : ( - + )} ) diff --git a/src/features/location/lib/geo.ts b/src/features/location/lib/geo.ts index 81dffb9..1f4949a 100644 --- a/src/features/location/lib/geo.ts +++ b/src/features/location/lib/geo.ts @@ -31,6 +31,11 @@ export { BUNDESLAND_TO_PARLIAMENT } interface NominatimAddress { state?: string + city?: string + town?: string + village?: string + county?: string + suburb?: string } interface NominatimResponse { @@ -39,6 +44,7 @@ interface NominatimResponse { export interface GeoResult { bundesland: string | null + userCity: string | null landtag_label: string | null landtag_parliament_period_id: number | null mandates: MandateWithPolitician[] @@ -94,10 +100,18 @@ export async function detectFromCoords(db: PGlite, lat: number, lon: number, ski const data = (await res.json()) as NominatimResponse const state = data.address?.state ?? null + const userCity = data.address?.city ?? data.address?.town ?? data.address?.village ?? data.address?.county ?? null const entry = state ? (BUNDESLAND_TO_PARLIAMENT[state] ?? null) : null if (!state || !entry) { - return { bundesland: state, landtag_label: null, landtag_parliament_period_id: null, mandates: [], cachedAt: null } + return { + bundesland: state, + userCity, + landtag_label: null, + landtag_parliament_period_id: null, + mandates: [], + cachedAt: null, + } } if (!skipCache) { @@ -114,6 +128,7 @@ export async function detectFromCoords(db: PGlite, lat: number, lon: number, ski const result: GeoResult = { bundesland: state, + userCity, landtag_label: entry.label, landtag_parliament_period_id: entry.parliamentPeriodId, mandates, diff --git a/src/shared/components/representative-list.tsx b/src/shared/components/representative-list.tsx index 553f718..aa5ef49 100644 --- a/src/shared/components/representative-list.tsx +++ b/src/shared/components/representative-list.tsx @@ -18,7 +18,14 @@ function partyLabel(m: MandateWithPolitician): string { return "parteilos" } -export function groupByParty(mandates: MandateWithPolitician[]): PartyGroup[] { +/** Check if a mandate's constituency label contains the user's city name. */ +function isLocalConstituency(m: MandateWithPolitician, city: string): boolean { + const label = m.electoral_data?.constituency?.label + if (!label) return false + return label.toLowerCase().includes(city.toLowerCase()) +} + +export function groupByParty(mandates: MandateWithPolitician[], userCity?: string | null): PartyGroup[] { const map = new Map() for (const m of mandates) { const key = partyLabel(m) @@ -33,7 +40,14 @@ export function groupByParty(mandates: MandateWithPolitician[]): PartyGroup[] { .sort((a, b) => b[1].length - a[1].length) .map(([partyLabel, members]) => ({ partyLabel, - members: members.sort((a, b) => a.politician.label.localeCompare(b.politician.label)), + members: members.sort((a, b) => { + if (userCity) { + const aLocal = isLocalConstituency(a, userCity) ? 0 : 1 + const bLocal = isLocalConstituency(b, userCity) ? 0 : 1 + if (aLocal !== bLocal) return aLocal - bLocal + } + return a.politician.label.localeCompare(b.politician.label) + }), })) } @@ -52,18 +66,18 @@ export function mandateFunction(m: MandateWithPolitician): string | null { interface RepresentativeListProps { mandates: MandateWithPolitician[] - userBundesland?: string | null + userCity?: string | null searchQuery: string onSearchChange: (query: string) => void } -export function RepresentativeList({ mandates, searchQuery, onSearchChange }: RepresentativeListProps) { +export function RepresentativeList({ mandates, userCity, searchQuery, onSearchChange }: RepresentativeListProps) { const { isFollowing, follow, unfollow } = useFollows() const filtered = searchQuery ? mandates.filter((m) => m.politician.label.toLowerCase().includes(searchQuery.toLowerCase())) : mandates - const groups = groupByParty(filtered) + const groups = groupByParty(filtered, userCity) return (
@@ -100,11 +114,17 @@ export function RepresentativeList({ mandates, searchQuery, onSearchChange }: Re {group.members.map((m) => { const followed = isFollowing("politician", m.politician.id) const fn = mandateFunction(m) + const local = userCity ? isLocalConstituency(m, userCity) : false return (

{m.politician.label}

- {fn &&

{fn}

} + {fn && ( +

+ {fn} + {local && — Dein Wahlkreis} +

+ )}