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:
@@ -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>
|
||||||
|
|||||||
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user