"use client"; import { useState, useEffect } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { Bell, Rss, CheckCircle, Copy, RefreshCw, XCircle, FlaskConical } from "lucide-react"; import { notificationsAPI, type NotificationTestResult } from "@/lib/api"; const AUTH_METHODS = [ { value: "none", label: "No authentication", hint: "Public ntfy.sh topics or open self-hosted servers" }, { value: "token", label: "Access token", hint: "ntfy token (tk_...)" }, { value: "basic", label: "Username & password", hint: "For servers behind HTTP basic auth or nginx ACL" }, ]; export default function NotificationsPage() { const { data: settings, refetch } = useQuery({ queryKey: ["notification-settings"], queryFn: () => notificationsAPI.getSettings(), }); const update = useMutation({ mutationFn: (data: Parameters[0]) => notificationsAPI.updateSettings(data), onSuccess: () => refetch(), }); const resetRss = useMutation({ mutationFn: () => notificationsAPI.resetRssToken(), onSuccess: () => refetch(), }); // ntfy form state const [topicUrl, setTopicUrl] = useState(""); const [authMethod, setAuthMethod] = useState("none"); const [token, setToken] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [ntfySaved, setNtfySaved] = useState(false); // Test state const [ntfyTesting, setNtfyTesting] = useState(false); const [ntfyTestResult, setNtfyTestResult] = useState(null); const [rssTesting, setRssTesting] = useState(false); const [rssTestResult, setRssTestResult] = useState(null); // RSS state const [rssSaved, setRssSaved] = useState(false); const [copied, setCopied] = useState(false); // Populate from loaded settings useEffect(() => { if (!settings) return; setTopicUrl(settings.ntfy_topic_url ?? ""); setAuthMethod(settings.ntfy_auth_method ?? "none"); setToken(settings.ntfy_token ?? ""); setUsername(settings.ntfy_username ?? ""); setPassword(settings.ntfy_password ?? ""); }, [settings]); const saveNtfy = (enabled: boolean) => { update.mutate( { ntfy_topic_url: topicUrl, ntfy_auth_method: authMethod, ntfy_token: authMethod === "token" ? token : "", ntfy_username: authMethod === "basic" ? username : "", ntfy_password: authMethod === "basic" ? password : "", ntfy_enabled: enabled, }, { onSuccess: () => { setNtfySaved(true); setTimeout(() => setNtfySaved(false), 2000); } } ); }; const toggleRss = (enabled: boolean) => { update.mutate( { rss_enabled: enabled }, { onSuccess: () => { setRssSaved(true); setTimeout(() => setRssSaved(false), 2000); } } ); }; const testNtfy = async () => { setNtfyTesting(true); setNtfyTestResult(null); try { const result = await notificationsAPI.testNtfy({ ntfy_topic_url: topicUrl, ntfy_auth_method: authMethod, ntfy_token: authMethod === "token" ? token : "", ntfy_username: authMethod === "basic" ? username : "", ntfy_password: authMethod === "basic" ? password : "", }); setNtfyTestResult(result); } catch { setNtfyTestResult({ status: "error", detail: "Request failed — check your topic URL" }); } finally { setNtfyTesting(false); } }; const testRss = async () => { setRssTesting(true); setRssTestResult(null); try { const result = await notificationsAPI.testRss(); setRssTestResult(result); } catch { setRssTestResult({ status: "error", detail: "Feed check failed" }); } finally { setRssTesting(false); } }; const rssUrl = settings?.rss_token ? `${typeof window !== "undefined" ? window.location.origin : ""}/api/notifications/feed/${settings.rss_token}.xml` : null; return (

Notifications

Get alerted when bills you follow are updated, new text is published, or amendments are filed.

{/* ntfy */}

Push Notifications (ntfy)

Uses ntfy — a free, open-source push notification service. Use the public ntfy.sh server or your own self-hosted instance.

{settings?.ntfy_enabled && ( Active )}
{/* Topic URL */}

The full URL to your ntfy topic, e.g.{" "} https://ntfy.sh/my-pocketveto-alerts

setTopicUrl(e.target.value)} className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
{/* Auth method */}
{AUTH_METHODS.map(({ value, label, hint }) => ( ))}
{/* Token input */} {authMethod === "token" && (
setToken(e.target.value)} className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
)} {/* Basic auth inputs */} {authMethod === "basic" && (
setUsername(e.target.value)} className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
setPassword(e.target.value)} className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
)} {/* Actions */}
{settings?.ntfy_enabled && ( )}
{ntfyTestResult && (
{ntfyTestResult.status === "ok" ? : } {ntfyTestResult.detail}
)}
{/* RSS */}

RSS Feed

A private, tokenized RSS feed of your bill alerts — subscribe in any RSS reader. Independent of ntfy; enable either or both.

{settings?.rss_enabled && ( Active )}
{rssUrl && (
{rssUrl}
)}
{!settings?.rss_enabled ? ( ) : ( )} {rssUrl && ( <> )}
{rssTestResult && (
{rssTestResult.status === "ok" ? : } {rssTestResult.detail}
)}
); }