From c78006625317ed837b8817b2d3de84c0b9503363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Mon, 2 Mar 2026 13:23:21 +0100 Subject: [PATCH] restructure routes under app/, add konsta layout Co-Authored-By: Claude Opus 4.6 --- bun.lock | 6 ++ package.json | 2 + src/app.css | 1 + src/features/feed/components/feed-empty.tsx | 4 +- src/features/feed/components/feed-list.tsx | 42 ++++++++-- .../components/politician-search.tsx | 4 +- src/routes/__root.tsx | 81 +------------------ src/routes/app/feed.tsx | 78 ++++++++++++++++++ src/routes/{ => app}/politicians.tsx | 2 +- src/routes/app/route.tsx | 76 +++++++++++++++++ src/routes/{ => app}/settings.tsx | 2 +- src/routes/{ => app}/topics.tsx | 2 +- src/routes/feed.tsx | 73 ----------------- src/routes/index.tsx | 2 +- 14 files changed, 211 insertions(+), 164 deletions(-) create mode 100644 src/routes/app/feed.tsx rename src/routes/{ => app}/politicians.tsx (71%) create mode 100644 src/routes/app/route.tsx rename src/routes/{ => app}/settings.tsx (71%) rename src/routes/{ => app}/topics.tsx (70%) delete mode 100644 src/routes/feed.tsx diff --git a/bun.lock b/bun.lock index 3cb416b..151e459 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,11 @@ "": { "name": "abgeordnetenwatch-pwa", "dependencies": { + "@electric-sql/pglite": "^0.3.15", "@tanstack/react-router": "^1.120.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "konsta": "^5.0.7", "lucide-react": "^0.575.0", "radix-ui": "^1.4.3", "react": "^19.1.0", @@ -277,6 +279,8 @@ "@ecies/ciphers": ["@ecies/ciphers@0.2.5", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -1179,6 +1183,8 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "konsta": ["konsta@5.0.7", "", { "dependencies": { "tailwind-merge": "^3.3.1" } }, "sha512-4bc+5UmPkMTqbF9UndwF46oEePV/vADGdutVfRGo18n8lql2kv+K32Gi8xkFMuo6R8fCw016wVGUpRskkpnzmA=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], diff --git a/package.json b/package.json index 547c5e0..db1f4d6 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "prepare": "simple-git-hooks" }, "dependencies": { + "@electric-sql/pglite": "^0.3.15", "@tanstack/react-router": "^1.120.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "konsta": "^5.0.7", "lucide-react": "^0.575.0", "radix-ui": "^1.4.3", "react": "^19.1.0", diff --git a/src/app.css b/src/app.css index f9fc467..ffd4bc6 100644 --- a/src/app.css +++ b/src/app.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; +@import "konsta/react/theme.css"; @custom-variant dark (&:is(.dark *)); diff --git a/src/features/feed/components/feed-empty.tsx b/src/features/feed/components/feed-empty.tsx index bba9475..1f6fd0d 100644 --- a/src/features/feed/components/feed-empty.tsx +++ b/src/features/feed/components/feed-empty.tsx @@ -1,8 +1,8 @@ export function FeedEmpty() { return (
-

Your feed is empty

-

Follow topics or politicians in the other tabs to see polls here.

+

Dein Feed ist leer

+

Folge Themen oder Abgeordneten, um Abstimmungen hier zu sehen.

) } diff --git a/src/features/feed/components/feed-list.tsx b/src/features/feed/components/feed-list.tsx index 56e3b2d..11411cc 100644 --- a/src/features/feed/components/feed-list.tsx +++ b/src/features/feed/components/feed-list.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react" import type { FeedItem } from "../lib/assemble-feed" import { FeedEmpty } from "./feed-empty" import { FeedItemCard } from "./feed-item" @@ -5,11 +6,42 @@ import { FeedItemCard } from "./feed-item" export function FeedList({ items }: { items: FeedItem[] }) { if (items.length === 0) return + const { upcoming, past } = useMemo(() => { + const upcoming: FeedItem[] = [] + const past: FeedItem[] = [] + for (const item of items) { + if (item.status === "upcoming") upcoming.push(item) + else past.push(item) + } + return { upcoming, past } + }, [items]) + return ( -
- {items.map((item) => ( - - ))} -
+
+ {upcoming.length > 0 && ( +
+

+ Anstehende Abstimmungen +

+
+ {upcoming.map((item) => ( + + ))} +
+
+ )} + {past.length > 0 && ( +
+

+ Vergangene Abstimmungen +

+
+ {past.map((item) => ( + + ))} +
+
+ )} +
) } diff --git a/src/features/politicians/components/politician-search.tsx b/src/features/politicians/components/politician-search.tsx index 189bc8b..dcd4311 100644 --- a/src/features/politicians/components/politician-search.tsx +++ b/src/features/politicians/components/politician-search.tsx @@ -78,8 +78,8 @@ export function PoliticianSearch() {

No parliament members loaded yet. Detect your location in Settings first.

- - Go to Settings + + Zu den Einstellungen ) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 2d70130..f5c739d 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,84 +1,9 @@ -import { Link, Outlet, createRootRoute, useMatches } from "@tanstack/react-router" - -interface TabDef { - to: string - label: string - icon: string - ariaLabel: string -} - -const TABS: TabDef[] = [ - { - to: "/feed", - label: "Feed", - ariaLabel: "Feed", - icon: "M3 7h18M3 12h18M3 17h18", - }, - { - to: "/topics", - label: "Topics", - ariaLabel: "Topics", - 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: "/politicians", - label: "People", - ariaLabel: "Politicians", - 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: "/settings", - label: "Settings", - ariaLabel: "Settings", - icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z", - }, -] +import { Outlet, createRootRoute } from "@tanstack/react-router" function RootComponent() { - const matches = useMatches() - const currentPath = matches.at(-1)?.pathname ?? "/feed" - const currentTab = TABS.find((t) => currentPath.endsWith(t.to)) ?? TABS[0] - return ( -
-
-

Abgeordnetenwatch — {currentTab.label}

-
- -
- -
- - +
+
) } diff --git a/src/routes/app/feed.tsx b/src/routes/app/feed.tsx new file mode 100644 index 0000000..6c47cb0 --- /dev/null +++ b/src/routes/app/feed.tsx @@ -0,0 +1,78 @@ +import { FeedList, useFeed } from "@/features/feed" +import { createFileRoute } from "@tanstack/react-router" +import { Navbar } from "konsta/react" + +function formatCacheAge(timestamp: number): string { + const minutes = Math.floor((Date.now() - timestamp) / 60_000) + if (minutes < 1) return "gerade eben" + if (minutes < 60) return `vor ${minutes} Min.` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `vor ${hours} Std.` + const days = Math.floor(hours / 24) + return `vor ${days} T.` +} + +function FeedPage() { + const { items, loading, refreshing, error, lastUpdated, refresh } = useFeed() + const hasItems = items.length > 0 + + return ( + <> + refresh({ silent: true })} + disabled={refreshing} + className="p-1.5 rounded-md hover:bg-muted transition-colors disabled:opacity-50" + aria-label="Feed aktualisieren" + > + + + ) : undefined + } + subtitle={lastUpdated ? `Aktualisiert ${formatCacheAge(lastUpdated)}` : undefined} + /> + +
+ {/* Loading spinner — only when no cached items */} + {loading && !hasItems && ( + +
+ + )} + + {/* Error */} + {error && ( +
+

Fehler beim Laden

+

{error}

+
+ )} + + {/* Feed list */} + {hasItems && } +
+ + ) +} + +export const Route = createFileRoute("/app/feed")({ + component: FeedPage, +}) diff --git a/src/routes/politicians.tsx b/src/routes/app/politicians.tsx similarity index 71% rename from src/routes/politicians.tsx rename to src/routes/app/politicians.tsx index 8bc6a2e..97a335c 100644 --- a/src/routes/politicians.tsx +++ b/src/routes/app/politicians.tsx @@ -1,6 +1,6 @@ import { PoliticianSearch } from "@/features/politicians" import { createFileRoute } from "@tanstack/react-router" -export const Route = createFileRoute("/politicians")({ +export const Route = createFileRoute("/app/politicians")({ component: PoliticianSearch, }) diff --git a/src/routes/app/route.tsx b/src/routes/app/route.tsx new file mode 100644 index 0000000..33115f7 --- /dev/null +++ b/src/routes/app/route.tsx @@ -0,0 +1,76 @@ +import { Outlet, createFileRoute, useMatches, useNavigate } from "@tanstack/react-router" +import { App, Page, Tabbar, TabbarLink } from "konsta/react" + +interface TabDef { + to: string + label: string + icon: string +} + +const TABS: TabDef[] = [ + { + to: "/app/feed", + label: "Feed", + icon: "M3 7h18M3 12h18M3 17h18", + }, + { + 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/politicians", + label: "Abgeordnete", + 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/settings", + label: "Einstellungen", + icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z", + }, +] + +function TabIcon({ d }: { d: string }) { + return ( + + ) +} + +function AppLayout() { + const navigate = useNavigate() + const matches = useMatches() + const currentPath = matches.at(-1)?.pathname ?? "/app/feed" + + return ( + + + + + {TABS.map((tab) => ( + navigate({ to: tab.to })} + icon={} + label={tab.label} + /> + ))} + + + + ) +} + +export const Route = createFileRoute("/app")({ + component: AppLayout, +}) diff --git a/src/routes/settings.tsx b/src/routes/app/settings.tsx similarity index 71% rename from src/routes/settings.tsx rename to src/routes/app/settings.tsx index 04b92a6..abd6359 100644 --- a/src/routes/settings.tsx +++ b/src/routes/app/settings.tsx @@ -1,6 +1,6 @@ import { SettingsPage } from "@/features/settings" import { createFileRoute } from "@tanstack/react-router" -export const Route = createFileRoute("/settings")({ +export const Route = createFileRoute("/app/settings")({ component: SettingsPage, }) diff --git a/src/routes/topics.tsx b/src/routes/app/topics.tsx similarity index 70% rename from src/routes/topics.tsx rename to src/routes/app/topics.tsx index 8b3514f..bc0c8bf 100644 --- a/src/routes/topics.tsx +++ b/src/routes/app/topics.tsx @@ -1,6 +1,6 @@ import { TopicList } from "@/features/topics" import { createFileRoute } from "@tanstack/react-router" -export const Route = createFileRoute("/topics")({ +export const Route = createFileRoute("/app/topics")({ component: TopicList, }) diff --git a/src/routes/feed.tsx b/src/routes/feed.tsx deleted file mode 100644 index 86d574a..0000000 --- a/src/routes/feed.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { FeedList, useFeed } from "@/features/feed" -import { createFileRoute } from "@tanstack/react-router" - -function formatCacheAge(timestamp: number): string { - const minutes = Math.floor((Date.now() - timestamp) / 60_000) - if (minutes < 1) return "gerade eben" - if (minutes < 60) return `vor ${minutes} Min.` - const hours = Math.floor(minutes / 60) - if (hours < 24) return `vor ${hours} Std.` - const days = Math.floor(hours / 24) - return `vor ${days} T.` -} - -function FeedPage() { - const { items, loading, refreshing, error, lastUpdated, refresh } = useFeed() - const hasItems = items.length > 0 - - return ( -
- {/* Refresh bar */} - {lastUpdated && ( -
- Aktualisiert {formatCacheAge(lastUpdated)} - -
- )} - - {/* Loading spinner — only when no cached items */} - {loading && !hasItems && ( - -
- - )} - - {/* Error */} - {error && ( -
-

Fehler beim Laden

-

{error}

-
- )} - - {/* Feed list */} - {hasItems && } -
- ) -} - -export const Route = createFileRoute("/feed")({ - component: FeedPage, -}) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index b342d8d..386ee20 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,5 @@ import { Navigate, createFileRoute } from "@tanstack/react-router" export const Route = createFileRoute("/")({ - component: () => , + component: () => , })