sort constituency representatives matching user's city to top, show "Dein Wahlkreis" badge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<MandateWithPolitician[]>([])
|
||||
const [userCity, setUserCity] = useState<string | null>(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() {
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : mandates.length > 0 ? (
|
||||
<RepresentativeList mandates={mandates} searchQuery={politicianSearch} onSearchChange={setPoliticianSearch} />
|
||||
<RepresentativeList
|
||||
mandates={mandates}
|
||||
userCity={userCity}
|
||||
searchQuery={politicianSearch}
|
||||
onSearchChange={setPoliticianSearch}
|
||||
/>
|
||||
) : (
|
||||
<p className="px-4 py-6 text-sm text-muted-foreground text-center">Keine Abgeordneten verfügbar.</p>
|
||||
)}
|
||||
|
||||
@@ -11,11 +11,15 @@ export function LandtagConfigure() {
|
||||
const search = useLandtagUI((s) => s.politicianSearch)
|
||||
const setSearch = useLandtagUI((s) => s.setPoliticianSearch)
|
||||
const [mandates, setMandates] = useState<MandateWithPolitician[]>([])
|
||||
const [userCity, setUserCity] = useState<string | null>(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() {
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<RepresentativeList mandates={mandates} searchQuery={search} onSearchChange={setSearch} />
|
||||
<RepresentativeList mandates={mandates} userCity={userCity} searchQuery={search} onSearchChange={setSearch} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, MandateWithPolitician[]>()
|
||||
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 (
|
||||
<div className="pb-4">
|
||||
@@ -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 (
|
||||
<div key={m.id} className="flex items-center justify-between px-4 py-2.5">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{m.politician.label}</p>
|
||||
{fn && <p className="text-xs text-muted-foreground">{fn}</p>}
|
||||
{fn && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{fn}
|
||||
{local && <span className="ml-1.5 text-primary font-medium">— Dein Wahlkreis</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user