merge push + standort into one section with switches, move unfollow buttons to dev settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 22:50:13 +01:00
parent 9f691c711b
commit 22bf485b76
3 changed files with 100 additions and 87 deletions

View File

@@ -25,7 +25,7 @@ export function SettingsPage() {
const deviceId = useDeviceId() const deviceId = useDeviceId()
const { needRefresh, checkForUpdate, applyUpdate } = usePwaUpdate() const { needRefresh, checkForUpdate, applyUpdate } = usePwaUpdate()
const push = usePush() const push = usePush()
const { follows, follow, unfollowAll } = useFollows() const { follow, unfollowAllTopics, unfollowAllPoliticians } = useFollows()
const [checking, setChecking] = useState(false) const [checking, setChecking] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [result, setResult] = useState<GeoResult | null>(null) const [result, setResult] = useState<GeoResult | null>(null)
@@ -93,18 +93,16 @@ export function SettingsPage() {
return ( return (
<div className="px-4 py-4 space-y-6 pb-4"> <div className="px-4 py-4 space-y-6 pb-4">
{/* --- Notifications --- */} {/* --- Permissions: Push + Location --- */}
{VAPID_PUBLIC_KEY && ( <section>
<section> <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Berechtigungen</h2>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2"> <Card className="py-0 gap-0">
Benachrichtigungen <CardContent className="p-0 divide-y divide-border">
</h2> {VAPID_PUBLIC_KEY &&
<Card className="py-0 gap-0"> (push.permission === "denied" ? (
<CardContent className="p-0 divide-y divide-border">
{push.permission === "denied" ? (
<div className="px-4 py-3"> <div className="px-4 py-3">
<span className="text-destructive text-sm"> <span className="text-destructive text-sm">
Benachrichtigungen sind blockiert. Bitte in den Systemeinstellungen aktivieren. Push blockiert bitte in den Systemeinstellungen aktivieren.
</span> </span>
</div> </div>
) : ( ) : (
@@ -122,80 +120,57 @@ export function SettingsPage() {
}} }}
/> />
</div> </div>
)} ))}
{!standalone && ( <div className="flex items-center justify-between px-4 py-3">
<button <div>
type="button" <span className="text-sm" id="location-label">
className="w-full flex items-center justify-between px-4 py-3 text-sm hover:bg-muted transition-colors" Standort
onClick={() => setShowGuide(true)} </span>
{loading && (
<span className="ml-2 text-xs text-muted-foreground">
{hasLocation ? "Aktualisiere…" : "Erkenne…"}
</span>
)}
</div>
<Switch
checked={!!hasLocation}
disabled={loading}
aria-labelledby="location-label"
onCheckedChange={(checked) => {
if (checked) detect(false)
else handleClearCache()
}}
/>
</div>
{errorMsg && (
<div className="px-4 py-3">
<span className="text-destructive text-sm">{errorMsg}</span>
</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"
> >
Einrichtung auf dem iPhone <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
<svg </svg>
xmlns="http://www.w3.org/2000/svg" </button>
className="w-4 h-4 text-muted-foreground" )}
fill="none" </CardContent>
viewBox="0 0 24 24" </Card>
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
)}
</CardContent>
</Card>
</section>
)}
{/* --- Location --- */}
<section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Standort</h2>
{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>
)}
{errorMsg && (
<div className="mb-3 p-3 bg-destructive/10 rounded-lg text-destructive text-sm" role="alert">
{errorMsg}
</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> </section>
{/* --- Follows --- */}
{follows.length > 0 && (
<section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Follows</h2>
<div className="flex flex-col gap-2">
<Button size="sm" variant="outline" onClick={unfollowAll}>
Allen Themen und Abgeordneten entfolgen ({follows.length})
</Button>
</div>
</section>
)}
{/* --- App Update --- */} {/* --- App Update --- */}
<section> <section>
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">App-Update</h2> <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">App-Update</h2>
@@ -325,6 +300,12 @@ export function SettingsPage() {
</Button> </Button>
</div> </div>
</div> </div>
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Allen Themen entfolgen</span>
<Button size="sm" variant="outline" onClick={unfollowAllTopics}>
Entfolgen
</Button>
</div>
<div className="flex items-center justify-between px-4 py-3"> <div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Alle Abgeordnete folgen</span> <span className="text-sm">Alle Abgeordnete folgen</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -349,6 +330,18 @@ export function SettingsPage() {
</Button> </Button>
</div> </div>
</div> </div>
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Allen Abgeordneten entfolgen</span>
<Button size="sm" variant="outline" onClick={unfollowAllPoliticians}>
Entfolgen
</Button>
</div>
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Abgeordnete neu laden</span>
<Button size="sm" variant="outline" onClick={() => detect(true)} disabled={loading || !hasLocation}>
Neu laden
</Button>
</div>
</CardContent> </CardContent>
</Card> </Card>
</section> </section>

View File

@@ -28,7 +28,10 @@ export async function removeFollow(db: PGlite, type: "topic" | "politician", ent
await db.query("DELETE FROM follows WHERE type = $1 AND entity_id = $2", [type, entityId]) await db.query("DELETE FROM follows WHERE type = $1 AND entity_id = $2", [type, entityId])
} }
export async function removeAllFollows(db: PGlite): Promise<number> { export async function removeAllFollows(db: PGlite): Promise<void> {
const res = await db.query("DELETE FROM follows") await db.query("DELETE FROM follows")
return res.affectedRows ?? 0 }
export async function removeFollowsByType(db: PGlite, type: "topic" | "politician"): Promise<void> {
await db.query("DELETE FROM follows WHERE type = $1", [type])
} }

View File

@@ -1,4 +1,11 @@
import { type Follow, addFollow, getFollows, removeAllFollows, removeFollow } from "@/shared/db/follows" import {
type Follow,
addFollow,
getFollows,
removeAllFollows,
removeFollow,
removeFollowsByType,
} from "@/shared/db/follows"
import { useDb } from "@/shared/db/provider" import { useDb } from "@/shared/db/provider"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
@@ -54,5 +61,15 @@ export function useFollows() {
emitChange() emitChange()
}, [db]) }, [db])
return { follows, isFollowing, follow, unfollow, unfollowAll } const unfollowAllTopics = useCallback(async () => {
await removeFollowsByType(db, "topic")
emitChange()
}, [db])
const unfollowAllPoliticians = useCallback(async () => {
await removeFollowsByType(db, "politician")
emitChange()
}, [db])
return { follows, isFollowing, follow, unfollow, unfollowAll, unfollowAllTopics, unfollowAllPoliticians }
} }