remove konsta ui, use plain tailwind + shadcn components, add safe-area css

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 21:45:41 +01:00
parent 5c26e0458c
commit fa47c4fd07
7 changed files with 366 additions and 304 deletions

View File

@@ -9,7 +9,6 @@
"@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",
@@ -1183,8 +1182,6 @@
"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=="],

View File

@@ -17,7 +17,6 @@
"@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",

View File

@@ -1,7 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "konsta/react/theme.css";
@custom-variant dark (&:is(.dark *));
@@ -101,3 +100,11 @@
-webkit-font-smoothing: antialiased;
}
}
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}

View File

@@ -1,12 +1,10 @@
import { Block } from "konsta/react"
export function FeedEmpty() {
return (
<Block className="text-center mt-12">
<div className="text-center mt-12 px-4">
<p className="text-lg font-medium">Dein Feed ist leer</p>
<p className="text-sm text-black/55 dark:text-white/55 mt-2">
<p className="text-sm text-muted-foreground mt-2">
Folge Themen oder Abgeordneten, um Abstimmungen hier zu sehen.
</p>
</Block>
</div>
)
}

View File

@@ -1,4 +1,3 @@
import { BlockTitle, List } from "konsta/react"
import { useMemo } from "react"
import type { FeedItem } from "../lib/assemble-feed"
import { FeedEmpty } from "./feed-empty"
@@ -21,22 +20,26 @@ export function FeedList({ items }: { items: FeedItem[] }) {
<div>
{upcoming.length > 0 && (
<section>
<BlockTitle>Anstehende Abstimmungen</BlockTitle>
<List dividers>
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
Anstehende Abstimmungen
</h2>
<div className="divide-y divide-border">
{upcoming.map((item) => (
<FeedItemCard key={item.id} item={item} />
))}
</List>
</div>
</section>
)}
{past.length > 0 && (
<section>
<BlockTitle>Vergangene Abstimmungen</BlockTitle>
<List dividers>
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
Vergangene Abstimmungen
</h2>
<div className="divide-y divide-border">
{past.map((item) => (
<FeedItemCard key={item.id} item={item} />
))}
</List>
</div>
</section>
)}
</div>

View File

@@ -1,4 +1,4 @@
import { Block, Button, Navbar, NavbarBackLink } from "konsta/react"
import { Button } from "@/shared/components/ui/button"
interface NotificationGuideProps {
onBack: () => void
@@ -6,87 +6,104 @@ interface NotificationGuideProps {
export function NotificationGuide({ onBack }: NotificationGuideProps) {
return (
<>
<Navbar title="iPhone-Einrichtung" left={<NavbarBackLink onClick={onBack} text="Zurück" showText />} />
<div className="px-4 py-4">
<button
type="button"
onClick={onBack}
className="flex items-center gap-1 text-sm text-primary mb-4 hover:underline"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Zurück
</button>
<Block>
<p className="text-sm text-black/55 dark:text-white/55 mb-6">
Push-Benachrichtigungen funktionieren auf dem iPhone nur, wenn die App zum Homescreen hinzugefügt wurde.
</p>
<h2 className="text-lg font-semibold mb-4">iPhone-Einrichtung</h2>
<ol className="space-y-6">
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
1
</span>
<div>
<p className="text-sm font-medium">Teilen-Menü öffnen</p>
<p className="text-sm text-black/55 dark:text-white/55 mt-1">
Tippe auf das Teilen-Symbol{" "}
<svg
xmlns="http://www.w3.org/2000/svg"
className="inline w-4 h-4 align-text-bottom"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
</svg>{" "}
in der Safari-Leiste unten.
</p>
</div>
</li>
<p className="text-sm text-muted-foreground mb-6">
Push-Benachrichtigungen funktionieren auf dem iPhone nur, wenn die App zum Homescreen hinzugefügt wurde.
</p>
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
2
</span>
<div>
<p className="text-sm font-medium">Zum Home-Bildschirm</p>
<p className="text-sm text-black/55 dark:text-white/55 mt-1">
Scrolle im Menü nach unten und wähle <span className="font-medium">Zum Home-Bildschirm</span>.
</p>
</div>
</li>
<ol className="space-y-6">
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
1
</span>
<div>
<p className="text-sm font-medium">Teilen-Menü öffnen</p>
<p className="text-sm text-muted-foreground mt-1">
Tippe auf das Teilen-Symbol{" "}
<svg
xmlns="http://www.w3.org/2000/svg"
className="inline w-4 h-4 align-text-bottom"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
</svg>{" "}
in der Safari-Leiste unten.
</p>
</div>
</li>
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
3
</span>
<div>
<p className="text-sm font-medium">App hinzufügen</p>
<p className="text-sm text-black/55 dark:text-white/55 mt-1">
Bestätige mit <span className="font-medium">Hinzufügen</span>. Die App erscheint als Icon auf deinem
Homescreen.
</p>
</div>
</li>
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
2
</span>
<div>
<p className="text-sm font-medium">Zum Home-Bildschirm</p>
<p className="text-sm text-muted-foreground mt-1">
Scrolle im Menü nach unten und wähle <span className="font-medium">Zum Home-Bildschirm</span>.
</p>
</div>
</li>
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
4
</span>
<div>
<p className="text-sm font-medium">App öffnen & Benachrichtigungen aktivieren</p>
<p className="text-sm text-black/55 dark:text-white/55 mt-1">
Öffne die App vom Homescreen aus und aktiviere die Benachrichtigungen in den Einstellungen.
</p>
</div>
</li>
</ol>
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
3
</span>
<div>
<p className="text-sm font-medium">App hinzufügen</p>
<p className="text-sm text-muted-foreground mt-1">
Bestätige mit <span className="font-medium">Hinzufügen</span>. Die App erscheint als Icon auf deinem
Homescreen.
</p>
</div>
</li>
<div className="mt-8">
<Button outline onClick={onBack}>
Zurück zu den Einstellungen
</Button>
</div>
</Block>
</>
<li className="flex gap-3">
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-white text-sm font-semibold shrink-0">
4
</span>
<div>
<p className="text-sm font-medium">App öffnen & Benachrichtigungen aktivieren</p>
<p className="text-sm text-muted-foreground mt-1">
Öffne die App vom Homescreen aus und aktiviere die Benachrichtigungen in den Einstellungen.
</p>
</div>
</li>
</ol>
<div className="mt-8">
<Button variant="outline" onClick={onBack}>
Zurück zu den Einstellungen
</Button>
</div>
</div>
)
}

View File

@@ -1,3 +1,6 @@
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent } from "@/shared/components/ui/card"
import { Switch } from "@/shared/components/ui/switch"
import { useDb } from "@/shared/db/provider"
import { useDeviceId } from "@/shared/hooks/use-device-id"
import { useFollows } from "@/shared/hooks/use-follows"
@@ -5,7 +8,6 @@ import { usePush } from "@/shared/hooks/use-push"
import { usePwaUpdate } from "@/shared/hooks/use-pwa-update"
import { fetchTopics } from "@/shared/lib/aw-api"
import { BACKEND_URL, VAPID_PUBLIC_KEY } from "@/shared/lib/constants"
import { BlockTitle, Button, List, ListItem, Navbar, Preloader, Toggle } from "konsta/react"
import { useCallback, useEffect, useState } from "react"
import { type GeoResult, clearGeoCache, detectFromCoords, loadCachedResult } from "../../location/lib/geo"
import { NotificationGuide } from "./notification-guide"
@@ -100,241 +102,280 @@ export function SettingsPage() {
}
return (
<div className="pb-20">
<Navbar title="Einstellungen" />
<div className="px-4 py-4 space-y-6 pb-4">
{/* --- Notifications --- */}
{VAPID_PUBLIC_KEY && (
<>
<BlockTitle>Benachrichtigungen</BlockTitle>
<List inset strong>
{push.permission === "denied" ? (
<ListItem
title={
<span className="text-red-500">
<section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
Benachrichtigungen
</h2>
<Card className="py-0 gap-0">
<CardContent className="p-0 divide-y divide-border">
{push.permission === "denied" ? (
<div className="px-4 py-3">
<span className="text-destructive text-sm">
Benachrichtigungen sind blockiert. Bitte in den Systemeinstellungen aktivieren.
</span>
}
/>
) : (
<ListItem
label
title="Push-Benachrichtigungen"
after={
<Toggle
</div>
) : (
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm" id="push-label">
Push-Benachrichtigungen
</span>
<Switch
checked={push.subscribed}
disabled={push.loading}
onChange={() => {
aria-labelledby="push-label"
onCheckedChange={() => {
if (push.subscribed) push.unsubscribe()
else push.subscribe()
}}
/>
}
/>
)}
{!standalone && <ListItem link title="Einrichtung auf dem iPhone" onClick={() => setShowGuide(true)} />}
</List>
</>
</div>
)}
{!standalone && (
<button
type="button"
className="w-full flex items-center justify-between px-4 py-3 text-sm hover:bg-muted transition-colors"
onClick={() => setShowGuide(true)}
>
Einrichtung auf dem iPhone
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 text-muted-foreground"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
)}
</CardContent>
</Card>
</section>
)}
{/* --- Location --- */}
<BlockTitle>Standort</BlockTitle>
{hasLocation && result.bundesland ? (
<List inset strong>
<ListItem title="Bundesland" after={result.bundesland} />
{result.landtag_label && <ListItem title="Landtag" after={result.landtag_label} />}
<ListItem title="Abgeordnete geladen" after={String(result.mandates.length)} />
{result.cachedAt && <ListItem title="Zwischengespeichert" after={formatCacheAge(result.cachedAt)} />}
</List>
) : null}
<section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Standort</h2>
{hasLocation && result.bundesland ? (
<Card className="py-0 gap-0 mb-3">
<CardContent className="p-0 divide-y divide-border">
<div className="flex justify-between px-4 py-3">
<span className="text-sm">Bundesland</span>
<span className="text-sm text-muted-foreground">{result.bundesland}</span>
</div>
{result.landtag_label && (
<div className="flex justify-between px-4 py-3">
<span className="text-sm">Landtag</span>
<span className="text-sm text-muted-foreground">{result.landtag_label}</span>
</div>
)}
<div className="flex justify-between px-4 py-3">
<span className="text-sm">Abgeordnete geladen</span>
<span className="text-sm text-muted-foreground">{result.mandates.length}</span>
</div>
{result.cachedAt && (
<div className="flex justify-between px-4 py-3">
<span className="text-sm">Zwischengespeichert</span>
<span className="text-sm text-muted-foreground">{formatCacheAge(result.cachedAt)}</span>
</div>
)}
</CardContent>
</Card>
) : null}
{loading && (
<div className="flex items-center justify-center h-16 mb-3">
<Preloader />
<span className="ml-3 text-sm text-black/55 dark:text-white/55">
{hasLocation ? "Aktualisiere…" : "Erkenne…"}
</span>
</div>
)}
{errorMsg && (
<div className="mx-4 mb-3 p-3 bg-red-500/10 rounded-lg text-red-500 text-sm" role="alert">
{errorMsg}
</div>
)}
<div className="flex flex-col gap-2 px-4">
{!hasLocation && !loading && (
<Button large onClick={() => detect(false)}>
Standort erkennen
</Button>
{loading && (
<div className="flex items-center justify-center h-16 mb-3">
<div className="w-6 h-6 border-3 border-primary border-t-transparent rounded-full animate-spin" />
<span className="ml-3 text-sm text-muted-foreground">{hasLocation ? "Aktualisiere…" : "Erkenne…"}</span>
</div>
)}
{hasLocation && (
<>
<Button small outline onClick={() => detect(true)} disabled={loading}>
Abgeordnete neu laden
</Button>
<Button small outline onClick={handleClearCache} disabled={loading}>
Cache löschen
</Button>
</>
{errorMsg && (
<div className="mb-3 p-3 bg-destructive/10 rounded-lg text-destructive text-sm" role="alert">
{errorMsg}
</div>
)}
</div>
<div className="flex flex-col gap-2">
{!hasLocation && !loading && (
<Button size="lg" onClick={() => detect(false)}>
Standort erkennen
</Button>
)}
{hasLocation && (
<>
<Button size="sm" variant="outline" onClick={() => detect(true)} disabled={loading}>
Abgeordnete neu laden
</Button>
<Button size="sm" variant="outline" onClick={handleClearCache} disabled={loading}>
Cache löschen
</Button>
</>
)}
</div>
</section>
{/* --- App Update --- */}
<BlockTitle className="mt-8">App-Update</BlockTitle>
<List inset strong>
{needRefresh ? (
<ListItem
title="Neue Version verfügbar"
after={
<Button small onClick={applyUpdate}>
Jetzt aktualisieren
</Button>
}
/>
) : (
<ListItem
title="App ist aktuell"
after={
<Button small outline onClick={handleCheckUpdate} disabled={checking}>
{checking ? "Prüfe…" : "Prüfen"}
</Button>
}
/>
)}
</List>
<section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">App-Update</h2>
<Card className="py-0 gap-0">
<CardContent className="p-0">
{needRefresh ? (
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Neue Version verfügbar</span>
<Button size="sm" onClick={applyUpdate}>
Jetzt aktualisieren
</Button>
</div>
) : (
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">App ist aktuell</span>
<Button size="sm" variant="outline" onClick={handleCheckUpdate} disabled={checking}>
{checking ? "Prüfe…" : "Prüfen"}
</Button>
</div>
)}
</CardContent>
</Card>
</section>
{/* --- About --- */}
<BlockTitle>Info</BlockTitle>
<List inset strong>
<ListItem
title="Datenquelle"
after={
<a
href="https://www.abgeordnetenwatch.de"
target="_blank"
rel="noopener noreferrer"
className="text-primary"
>
abgeordnetenwatch.de
</a>
}
/>
<ListItem
title="Geräte-ID"
after={<span className="font-mono text-xs max-w-[50%] truncate">{deviceId}</span>}
/>
</List>
<section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Info</h2>
<Card className="py-0 gap-0">
<CardContent className="p-0 divide-y divide-border">
<div className="flex justify-between px-4 py-3">
<span className="text-sm">Datenquelle</span>
<a
href="https://www.abgeordnetenwatch.de"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary"
>
abgeordnetenwatch.de
</a>
</div>
<div className="flex justify-between px-4 py-3">
<span className="text-sm">Geräte-ID</span>
<span className="font-mono text-xs max-w-[50%] truncate">{deviceId}</span>
</div>
</CardContent>
</Card>
</section>
{/* --- Developer --- */}
<BlockTitle>Entwickler</BlockTitle>
<List inset strong>
<ListItem
title="Backend Health"
after={
<div className="flex items-center gap-2">
{devHealth && (
<span className={`text-xs ${devHealth === "ok" ? "text-green-600" : "text-red-500"}`}>{devHealth}</span>
)}
<Button
small
outline
onClick={async () => {
setDevHealth(null)
try {
const res = await fetch(`${BACKEND_URL}/health`)
setDevHealth(res.ok ? "ok" : `${res.status}`)
} catch (e) {
setDevHealth(String(e))
}
}}
>
Prüfen
</Button>
<section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Entwickler</h2>
<Card className="py-0 gap-0">
<CardContent className="p-0 divide-y divide-border">
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Backend Health</span>
<div className="flex items-center gap-2">
{devHealth && (
<span className={`text-xs ${devHealth === "ok" ? "text-green-600" : "text-destructive"}`}>
{devHealth}
</span>
)}
<Button
size="sm"
variant="outline"
onClick={async () => {
setDevHealth(null)
try {
const res = await fetch(`${BACKEND_URL}/health`)
setDevHealth(res.ok ? "ok" : `${res.status}`)
} catch (e) {
setDevHealth(String(e))
}
}}
>
Prüfen
</Button>
</div>
</div>
}
/>
<ListItem
title="Test-Push"
after={
<div className="flex items-center gap-2">
{devPush && (
<span className={`text-xs ${devPush === "ok" ? "text-green-600" : "text-red-500"}`}>{devPush}</span>
)}
<Button
small
outline
onClick={async () => {
setDevPush(null)
try {
const res = await fetch(`${BACKEND_URL}/push/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ device_id: deviceId }),
})
setDevPush(res.ok ? "ok" : `${res.status}`)
} catch (e) {
setDevPush(String(e))
}
}}
>
Senden
</Button>
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Test-Push</span>
<div className="flex items-center gap-2">
{devPush && (
<span className={`text-xs ${devPush === "ok" ? "text-green-600" : "text-destructive"}`}>
{devPush}
</span>
)}
<Button
size="sm"
variant="outline"
onClick={async () => {
setDevPush(null)
try {
const res = await fetch(`${BACKEND_URL}/push/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ device_id: deviceId }),
})
setDevPush(res.ok ? "ok" : `${res.status}`)
} catch (e) {
setDevPush(String(e))
}
}}
>
Senden
</Button>
</div>
</div>
}
/>
<ListItem
title="Alle Themen folgen"
after={
<div className="flex items-center gap-2">
{devTopics && <span className="text-xs text-green-600">{devTopics}</span>}
<Button
small
outline
onClick={async () => {
setDevTopics(null)
try {
const topics = await fetchTopics()
for (const t of topics) follow("topic", t.id, t.label)
setDevTopics(`${topics.length}`)
} catch (e) {
setDevTopics(String(e))
}
}}
>
Folgen
</Button>
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Alle Themen folgen</span>
<div className="flex items-center gap-2">
{devTopics && <span className="text-xs text-green-600">{devTopics}</span>}
<Button
size="sm"
variant="outline"
onClick={async () => {
setDevTopics(null)
try {
const topics = await fetchTopics()
for (const t of topics) follow("topic", t.id, t.label)
setDevTopics(`${topics.length}`)
} catch (e) {
setDevTopics(String(e))
}
}}
>
Folgen
</Button>
</div>
</div>
}
/>
<ListItem
title="Alle Abgeordnete folgen"
after={
<div className="flex items-center gap-2">
{devPoliticians && <span className="text-xs text-green-600">{devPoliticians}</span>}
<Button
small
outline
onClick={async () => {
setDevPoliticians(null)
const cached = await loadCachedResult(db)
if (!cached || cached.mandates.length === 0) {
setDevPoliticians("Kein Standort-Cache")
return
}
for (const m of cached.mandates) {
follow("politician", m.politician.id, m.politician.label)
}
setDevPoliticians(`${cached.mandates.length}`)
}}
>
Folgen
</Button>
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Alle Abgeordnete folgen</span>
<div className="flex items-center gap-2">
{devPoliticians && <span className="text-xs text-green-600">{devPoliticians}</span>}
<Button
size="sm"
variant="outline"
onClick={async () => {
setDevPoliticians(null)
const cached = await loadCachedResult(db)
if (!cached || cached.mandates.length === 0) {
setDevPoliticians("Kein Standort-Cache")
return
}
for (const m of cached.mandates) {
follow("politician", m.politician.id, m.politician.label)
}
setDevPoliticians(`${cached.mandates.length}`)
}}
>
Folgen
</Button>
</div>
</div>
}
/>
</List>
</CardContent>
</Card>
</section>
</div>
)
}