fix polls API call, move themen back to tabbar, configure only for MdB/MdL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 21:38:14 +01:00
parent 3e1d542260
commit 5c26e0458c
13 changed files with 90 additions and 128 deletions

View File

@@ -1,5 +1,4 @@
import { RepresentativeList } from "@/shared/components/representative-list" import { RepresentativeList } from "@/shared/components/representative-list"
import { TopicToggleList } from "@/shared/components/topic-toggle-list"
import { useDb } from "@/shared/db/provider" import { useDb } from "@/shared/db/provider"
import type { MandateWithPolitician } from "@/shared/lib/aw-api" import type { MandateWithPolitician } from "@/shared/lib/aw-api"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
@@ -8,8 +7,6 @@ import { useBundestagUI } from "../store"
export function BundestagConfigure() { export function BundestagConfigure() {
const db = useDb() const db = useDb()
const topicSearch = useBundestagUI((s) => s.topicSearch)
const setTopicSearch = useBundestagUI((s) => s.setTopicSearch)
const politicianSearch = useBundestagUI((s) => s.politicianSearch) const politicianSearch = useBundestagUI((s) => s.politicianSearch)
const setPoliticianSearch = useBundestagUI((s) => s.setPoliticianSearch) const setPoliticianSearch = useBundestagUI((s) => s.setPoliticianSearch)
@@ -25,25 +22,15 @@ export function BundestagConfigure() {
return ( return (
<div className="pb-4"> <div className="pb-4">
<section> {loadingMandates ? (
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">Themen</h2> <div className="flex items-center justify-center h-48">
<TopicToggleList searchQuery={topicSearch} onSearchChange={setTopicSearch} /> <div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
</section> </div>
) : mandates.length > 0 ? (
<section> <RepresentativeList mandates={mandates} searchQuery={politicianSearch} onSearchChange={setPoliticianSearch} />
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide"> ) : (
Abgeordnete <p className="px-4 py-6 text-sm text-muted-foreground text-center">Keine Abgeordneten verfügbar.</p>
</h2> )}
{loadingMandates ? (
<div className="flex items-center justify-center h-48">
<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} />
) : (
<p className="px-4 text-sm text-muted-foreground">Keine Abgeordneten verfügbar.</p>
)}
</section>
</div> </div>
) )
} }

View File

@@ -2,12 +2,11 @@ import type { FeedItem } from "@/features/feed/lib/assemble-feed"
import { import {
type Poll, type Poll,
fetchCandidacyMandates, fetchCandidacyMandates,
fetchPolls,
fetchPollsByIds, fetchPollsByIds,
fetchPollsByLegislature,
fetchTopics, fetchTopics,
fetchVotes, fetchVotes,
} from "@/shared/lib/aw-api" } from "@/shared/lib/aw-api"
import { BUNDESTAG_LEGISLATURE_ID } from "@/shared/lib/constants"
import { fetchUpcomingVorgaenge } from "@/shared/lib/dip-api" import { fetchUpcomingVorgaenge } from "@/shared/lib/dip-api"
export async function assembleBundestagFeed( export async function assembleBundestagFeed(
@@ -16,7 +15,7 @@ export async function assembleBundestagFeed(
): Promise<FeedItem[]> { ): Promise<FeedItem[]> {
const [topics, polls, vorgaenge] = await Promise.all([ const [topics, polls, vorgaenge] = await Promise.all([
fetchTopics(), fetchTopics(),
fetchPollsByLegislature(BUNDESTAG_LEGISLATURE_ID, 150), fetchPolls(150),
fetchUpcomingVorgaenge().catch(() => []), fetchUpcomingVorgaenge().catch(() => []),
]) ])

View File

@@ -1,15 +1,11 @@
import { create } from "zustand" import { create } from "zustand"
interface BundestagUIState { interface BundestagUIState {
topicSearch: string
setTopicSearch: (query: string) => void
politicianSearch: string politicianSearch: string
setPoliticianSearch: (query: string) => void setPoliticianSearch: (query: string) => void
} }
export const useBundestagUI = create<BundestagUIState>((set) => ({ export const useBundestagUI = create<BundestagUIState>((set) => ({
topicSearch: "",
setTopicSearch: (query) => set({ topicSearch: query }),
politicianSearch: "", politicianSearch: "",
setPoliticianSearch: (query) => set({ politicianSearch: query }), setPoliticianSearch: (query) => set({ politicianSearch: query }),
})) }))

View File

@@ -1,5 +1,4 @@
import { RepresentativeList } from "@/shared/components/representative-list" import { RepresentativeList } from "@/shared/components/representative-list"
import { TopicToggleList } from "@/shared/components/topic-toggle-list"
import { useDb } from "@/shared/db/provider" import { useDb } from "@/shared/db/provider"
import type { MandateWithPolitician } from "@/shared/lib/aw-api" import type { MandateWithPolitician } from "@/shared/lib/aw-api"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
@@ -9,10 +8,8 @@ import { useLandtagUI } from "../store"
export function LandtagConfigure() { export function LandtagConfigure() {
const db = useDb() const db = useDb()
const topicSearch = useLandtagUI((s) => s.topicSearch) const search = useLandtagUI((s) => s.politicianSearch)
const setTopicSearch = useLandtagUI((s) => s.setTopicSearch) const setSearch = useLandtagUI((s) => s.setPoliticianSearch)
const politicianSearch = useLandtagUI((s) => s.politicianSearch)
const setPoliticianSearch = useLandtagUI((s) => s.setPoliticianSearch)
const [mandates, setMandates] = useState<MandateWithPolitician[]>([]) const [mandates, setMandates] = useState<MandateWithPolitician[]>([])
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
@@ -23,34 +20,28 @@ export function LandtagConfigure() {
}) })
}, [db]) }, [db])
if (loaded && mandates.length === 0) {
return (
<div className="text-center mt-12 px-4">
<p className="text-muted-foreground text-sm mb-4">
Noch keine Abgeordneten geladen. Erkenne zuerst deinen Standort in den Einstellungen.
</p>
<Link to="/app/settings" className="text-primary text-sm underline">
Zu den Einstellungen
</Link>
</div>
)
}
return ( return (
<div className="pb-4"> <div className="pb-4">
<section> {!loaded ? (
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">Themen</h2> <div className="flex items-center justify-center h-48">
<TopicToggleList searchQuery={topicSearch} onSearchChange={setTopicSearch} /> <div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
</section> </div>
) : (
<section> <RepresentativeList mandates={mandates} searchQuery={search} onSearchChange={setSearch} />
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide"> )}
Abgeordnete
</h2>
{!loaded ? (
<div className="flex items-center justify-center h-48">
<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} />
) : (
<div className="text-center px-4 py-6">
<p className="text-muted-foreground text-sm mb-4">
Noch keine Abgeordneten geladen. Erkenne zuerst deinen Standort in den Einstellungen.
</p>
<Link to="/app/settings" className="text-primary text-sm underline">
Zu den Einstellungen
</Link>
</div>
)}
</section>
</div> </div>
) )
} }

View File

@@ -1,55 +1,8 @@
import { RepresentativeList } from "@/shared/components/representative-list"
import { useDb } from "@/shared/db/provider"
import type { MandateWithPolitician } from "@/shared/lib/aw-api"
import { Link } from "@tanstack/react-router"
import { useEffect, useState } from "react"
import { type GeoResult, loadCachedResult } from "../../location/lib/geo"
import { useLandtagUI } from "../store"
export function LandtagPage() { export function LandtagPage() {
const db = useDb()
const [result, setResult] = useState<GeoResult | null>(null)
const [mandates, setMandates] = useState<MandateWithPolitician[]>([])
const search = useLandtagUI((s) => s.politicianSearch)
const setSearch = useLandtagUI((s) => s.setPoliticianSearch)
useEffect(() => {
loadCachedResult(db).then((cached) => {
if (cached) {
setResult(cached)
setMandates(cached.mandates)
}
})
}, [db])
if (!result || mandates.length === 0) {
return (
<div className="text-center mt-12 px-4">
<p className="text-lg font-medium">Landtag</p>
<p className="text-muted-foreground text-sm mt-2 mb-4">
Erkenne zuerst deinen Standort in den Einstellungen, um deine Landtagsabgeordneten zu sehen.
</p>
<Link to="/app/settings" className="text-primary text-sm underline">
Zu den Einstellungen
</Link>
</div>
)
}
return ( return (
<div className="pb-4"> <div className="text-center mt-12 px-4">
{result.landtag_label && ( <p className="text-lg font-medium">Landtag</p>
<div className="px-4 py-3 border-b border-border"> <p className="text-sm text-muted-foreground mt-2">Abstimmungsdaten folgen in Kürze.</p>
<p className="text-sm font-medium">{result.landtag_label}</p>
{result.bundesland && <p className="text-xs text-muted-foreground">{result.bundesland}</p>}
</div>
)}
<RepresentativeList mandates={mandates} searchQuery={search} onSearchChange={setSearch} />
<div className="px-4 py-6 text-center">
<p className="text-sm text-muted-foreground">Abstimmungsdaten folgen in Kürze</p>
</div>
</div> </div>
) )
} }

View File

@@ -1,15 +1,11 @@
import { create } from "zustand" import { create } from "zustand"
interface LandtagUIState { interface LandtagUIState {
topicSearch: string
setTopicSearch: (query: string) => void
politicianSearch: string politicianSearch: string
setPoliticianSearch: (query: string) => void setPoliticianSearch: (query: string) => void
} }
export const useLandtagUI = create<LandtagUIState>((set) => ({ export const useLandtagUI = create<LandtagUIState>((set) => ({
topicSearch: "",
setTopicSearch: (query) => set({ topicSearch: query }),
politicianSearch: "", politicianSearch: "",
setPoliticianSearch: (query) => set({ politicianSearch: query }), setPoliticianSearch: (query) => set({ politicianSearch: query }),
})) }))

View File

@@ -0,0 +1,9 @@
import { TopicToggleList } from "@/shared/components/topic-toggle-list"
import { useTopicsUI } from "../store"
export function TopicList() {
const search = useTopicsUI((s) => s.searchQuery)
const setSearch = useTopicsUI((s) => s.setSearchQuery)
return <TopicToggleList searchQuery={search} onSearchChange={setSearch} />
}

View File

@@ -0,0 +1 @@
export { TopicList } from "./components/topic-list"

View File

@@ -0,0 +1,11 @@
import { create } from "zustand"
interface TopicsUIState {
searchQuery: string
setSearchQuery: (query: string) => void
}
export const useTopicsUI = create<TopicsUIState>((set) => ({
searchQuery: "",
setSearchQuery: (query) => set({ searchQuery: query }),
}))

View File

@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as AppRouteRouteImport } from './routes/app/route' import { Route as AppRouteRouteImport } from './routes/app/route'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AppTopicsRouteImport } from './routes/app/topics'
import { Route as AppSettingsRouteImport } from './routes/app/settings' import { Route as AppSettingsRouteImport } from './routes/app/settings'
import { Route as AppHomeRouteImport } from './routes/app/home' import { Route as AppHomeRouteImport } from './routes/app/home'
import { Route as AppLandtagRouteRouteImport } from './routes/app/landtag/route' import { Route as AppLandtagRouteRouteImport } from './routes/app/landtag/route'
@@ -30,6 +31,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AppTopicsRoute = AppTopicsRouteImport.update({
id: '/topics',
path: '/topics',
getParentRoute: () => AppRouteRoute,
} as any)
const AppSettingsRoute = AppSettingsRouteImport.update({ const AppSettingsRoute = AppSettingsRouteImport.update({
id: '/settings', id: '/settings',
path: '/settings', path: '/settings',
@@ -78,6 +84,7 @@ export interface FileRoutesByFullPath {
'/app/landtag': typeof AppLandtagRouteRouteWithChildren '/app/landtag': typeof AppLandtagRouteRouteWithChildren
'/app/home': typeof AppHomeRoute '/app/home': typeof AppHomeRoute
'/app/settings': typeof AppSettingsRoute '/app/settings': typeof AppSettingsRoute
'/app/topics': typeof AppTopicsRoute
'/app/bundestag/configure': typeof AppBundestagConfigureRoute '/app/bundestag/configure': typeof AppBundestagConfigureRoute
'/app/landtag/configure': typeof AppLandtagConfigureRoute '/app/landtag/configure': typeof AppLandtagConfigureRoute
'/app/bundestag/': typeof AppBundestagIndexRoute '/app/bundestag/': typeof AppBundestagIndexRoute
@@ -88,6 +95,7 @@ export interface FileRoutesByTo {
'/app': typeof AppRouteRouteWithChildren '/app': typeof AppRouteRouteWithChildren
'/app/home': typeof AppHomeRoute '/app/home': typeof AppHomeRoute
'/app/settings': typeof AppSettingsRoute '/app/settings': typeof AppSettingsRoute
'/app/topics': typeof AppTopicsRoute
'/app/bundestag/configure': typeof AppBundestagConfigureRoute '/app/bundestag/configure': typeof AppBundestagConfigureRoute
'/app/landtag/configure': typeof AppLandtagConfigureRoute '/app/landtag/configure': typeof AppLandtagConfigureRoute
'/app/bundestag': typeof AppBundestagIndexRoute '/app/bundestag': typeof AppBundestagIndexRoute
@@ -101,6 +109,7 @@ export interface FileRoutesById {
'/app/landtag': typeof AppLandtagRouteRouteWithChildren '/app/landtag': typeof AppLandtagRouteRouteWithChildren
'/app/home': typeof AppHomeRoute '/app/home': typeof AppHomeRoute
'/app/settings': typeof AppSettingsRoute '/app/settings': typeof AppSettingsRoute
'/app/topics': typeof AppTopicsRoute
'/app/bundestag/configure': typeof AppBundestagConfigureRoute '/app/bundestag/configure': typeof AppBundestagConfigureRoute
'/app/landtag/configure': typeof AppLandtagConfigureRoute '/app/landtag/configure': typeof AppLandtagConfigureRoute
'/app/bundestag/': typeof AppBundestagIndexRoute '/app/bundestag/': typeof AppBundestagIndexRoute
@@ -115,6 +124,7 @@ export interface FileRouteTypes {
| '/app/landtag' | '/app/landtag'
| '/app/home' | '/app/home'
| '/app/settings' | '/app/settings'
| '/app/topics'
| '/app/bundestag/configure' | '/app/bundestag/configure'
| '/app/landtag/configure' | '/app/landtag/configure'
| '/app/bundestag/' | '/app/bundestag/'
@@ -125,6 +135,7 @@ export interface FileRouteTypes {
| '/app' | '/app'
| '/app/home' | '/app/home'
| '/app/settings' | '/app/settings'
| '/app/topics'
| '/app/bundestag/configure' | '/app/bundestag/configure'
| '/app/landtag/configure' | '/app/landtag/configure'
| '/app/bundestag' | '/app/bundestag'
@@ -137,6 +148,7 @@ export interface FileRouteTypes {
| '/app/landtag' | '/app/landtag'
| '/app/home' | '/app/home'
| '/app/settings' | '/app/settings'
| '/app/topics'
| '/app/bundestag/configure' | '/app/bundestag/configure'
| '/app/landtag/configure' | '/app/landtag/configure'
| '/app/bundestag/' | '/app/bundestag/'
@@ -164,6 +176,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/app/topics': {
id: '/app/topics'
path: '/topics'
fullPath: '/app/topics'
preLoaderRoute: typeof AppTopicsRouteImport
parentRoute: typeof AppRouteRoute
}
'/app/settings': { '/app/settings': {
id: '/app/settings' id: '/app/settings'
path: '/settings' path: '/settings'
@@ -255,6 +274,7 @@ interface AppRouteRouteChildren {
AppLandtagRouteRoute: typeof AppLandtagRouteRouteWithChildren AppLandtagRouteRoute: typeof AppLandtagRouteRouteWithChildren
AppHomeRoute: typeof AppHomeRoute AppHomeRoute: typeof AppHomeRoute
AppSettingsRoute: typeof AppSettingsRoute AppSettingsRoute: typeof AppSettingsRoute
AppTopicsRoute: typeof AppTopicsRoute
} }
const AppRouteRouteChildren: AppRouteRouteChildren = { const AppRouteRouteChildren: AppRouteRouteChildren = {
@@ -262,6 +282,7 @@ const AppRouteRouteChildren: AppRouteRouteChildren = {
AppLandtagRouteRoute: AppLandtagRouteRouteWithChildren, AppLandtagRouteRoute: AppLandtagRouteRouteWithChildren,
AppHomeRoute: AppHomeRoute, AppHomeRoute: AppHomeRoute,
AppSettingsRoute: AppSettingsRoute, AppSettingsRoute: AppSettingsRoute,
AppTopicsRoute: AppTopicsRoute,
} }
const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren( const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(

View File

@@ -24,6 +24,11 @@ const TABS: TabDef[] = [
label: "Landtag", label: "Landtag",
icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z", icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z",
}, },
{
to: "/app/topics",
label: "Themen",
icon: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2",
},
{ {
to: "/app/settings", to: "/app/settings",
label: "Einstellungen", label: "Einstellungen",
@@ -87,7 +92,7 @@ function AppLayout() {
</button> </button>
) : null} ) : null}
<h1 className={`text-base font-semibold text-card-foreground ${isConfigureRoute ? "ml-2" : ""}`}> <h1 className={`text-base font-semibold text-card-foreground ${isConfigureRoute ? "ml-2" : ""}`}>
{isConfigureRoute ? `${currentTab.label} konfigurieren` : currentTab.label} {isConfigureRoute ? "Abgeordnete" : currentTab.label}
</h1> </h1>
{!isConfigureRoute && configureTarget && ( {!isConfigureRoute && configureTarget && (
<Link <Link

View File

@@ -0,0 +1,6 @@
import { TopicList } from "@/features/topics"
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/app/topics")({
component: TopicList,
})

View File

@@ -165,19 +165,6 @@ export function fetchVotes(mandateID: number): Promise<Vote[]> {
return request("votes", { mandate: String(mandateID), range_end: "200" }, voteSchema) return request("votes", { mandate: String(mandateID), range_end: "200" }, voteSchema)
} }
export function fetchPollsByLegislature(legislatureId: number, rangeEnd = 150): Promise<Poll[]> {
return request(
"polls",
{
parliament_period: String(legislatureId),
range_end: String(rangeEnd),
sort_by: "field_poll_date",
sort_direction: "desc",
},
pollSchema,
)
}
export function fetchMandatesByParliamentPeriod(periodID: number): Promise<MandateWithPolitician[]> { export function fetchMandatesByParliamentPeriod(periodID: number): Promise<MandateWithPolitician[]> {
return request( return request(
"candidacies-mandates", "candidacies-mandates",