diff --git a/after-dismiss.png b/after-dismiss.png new file mode 100644 index 0000000..72a7cc9 Binary files /dev/null and b/after-dismiss.png differ diff --git a/after-reload.png b/after-reload.png new file mode 100644 index 0000000..ebe0f7f Binary files /dev/null and b/after-reload.png differ diff --git a/client/src/components/push-notification-bell.tsx b/client/src/components/push-notification-bell.tsx index 55f6457..fa95bad 100644 --- a/client/src/components/push-notification-bell.tsx +++ b/client/src/components/push-notification-bell.tsx @@ -2,17 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { Bell, BellRing } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; - -function urlBase64ToUint8Array(base64String: string) { - const padding = "=".repeat((4 - (base64String.length % 4)) % 4); - const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -} +import { isPushSupported, getExistingSubscription, subscribeToPush, unsubscribeFromPush } from "@/lib/push-utils"; export default function PushNotificationBell() { const [isSubscribed, setIsSubscribed] = useState(false); @@ -21,19 +11,10 @@ export default function PushNotificationBell() { const { toast } = useToast(); const checkSubscription = useCallback(async () => { - if (!("serviceWorker" in navigator) || !("PushManager" in window)) return; + if (!isPushSupported()) return; setSupported(true); - try { - const reg = await navigator.serviceWorker.getRegistration("/sw.js"); - if (reg) { - const sub = await reg.pushManager.getSubscription(); - setIsSubscribed(!!sub); - } else { - setIsSubscribed(false); - } - } catch { - setIsSubscribed(false); - } + const sub = await getExistingSubscription(); + setIsSubscribed(!!sub); }, []); useEffect(() => { @@ -43,32 +24,10 @@ export default function PushNotificationBell() { const subscribe = async () => { setIsLoading(true); try { - const keyRes = await fetch("/api/push/vapid-key"); - const { publicKey } = await keyRes.json(); - if (!publicKey) throw new Error("VAPID key not configured"); - - const reg = await navigator.serviceWorker.register("/sw.js"); - await navigator.serviceWorker.ready; - - const sub = await reg.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(publicKey), - }); - - const subJson = sub.toJSON(); - const res = await fetch("/api/push/subscribe", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - endpoint: subJson.endpoint, - keys: subJson.keys, - }), - }); - if (!res.ok) throw new Error("Subscription save failed"); - + await subscribeToPush(); setIsSubscribed(true); toast({ title: "Benachrichtigungen aktiviert", description: "Sie werden über neue Inhalte informiert." }); - } catch (err: any) { + } catch { if (Notification.permission === "denied") { toast({ title: "Benachrichtigungen blockiert", description: "Bitte erlauben Sie Benachrichtigungen in Ihren Browsereinstellungen.", variant: "destructive" }); } else { @@ -82,19 +41,7 @@ export default function PushNotificationBell() { const unsubscribe = async () => { setIsLoading(true); try { - const reg = await navigator.serviceWorker.getRegistration("/sw.js"); - if (reg) { - const sub = await reg.pushManager.getSubscription(); - if (sub) { - const res = await fetch("/api/push/unsubscribe", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ endpoint: sub.endpoint }), - }); - if (!res.ok) throw new Error("Unsubscribe failed"); - await sub.unsubscribe(); - } - } + await unsubscribeFromPush(); setIsSubscribed(false); toast({ title: "Benachrichtigungen deaktiviert", description: "Sie erhalten keine weiteren Benachrichtigungen." }); } catch { diff --git a/client/src/components/push-prompt-banner.tsx b/client/src/components/push-prompt-banner.tsx new file mode 100644 index 0000000..621c59a --- /dev/null +++ b/client/src/components/push-prompt-banner.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from "react"; +import { Bell, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/hooks/use-toast"; +import { isPushSupported, getExistingSubscription, subscribeToPush } from "@/lib/push-utils"; + +const DISMISS_KEY = "folx-push-prompt-dismissed"; + +export default function PushPromptBanner() { + const [visible, setVisible] = useState(false); + const { toast } = useToast(); + + useEffect(() => { + if (!isPushSupported()) return; + if (localStorage.getItem(DISMISS_KEY)) return; + if (Notification.permission === "denied") return; + + const timer = setTimeout(async () => { + const sub = await getExistingSubscription(); + if (!sub) setVisible(true); + }, 5000); + + return () => clearTimeout(timer); + }, []); + + const dismiss = () => { + setVisible(false); + localStorage.setItem(DISMISS_KEY, "1"); + }; + + const accept = async () => { + try { + await subscribeToPush(); + toast({ title: "Benachrichtigungen aktiviert", description: "Sie werden über neue Inhalte informiert." }); + } catch { + if (Notification.permission === "denied") { + toast({ title: "Benachrichtigungen blockiert", description: "Bitte erlauben Sie Benachrichtigungen in Ihren Browsereinstellungen.", variant: "destructive" }); + } + } + dismiss(); + }; + + if (!visible) return null; + + return ( +
+
+
+
+ +
+
+

+ Nichts mehr verpassen! +

+

+ Erhalten Sie Benachrichtigungen über neue Artikel und Videos von FOLX TV. +

+
+ + +
+
+ +
+
+
+ ); +} diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 04d451e..30bf383 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -15,6 +15,7 @@ import { RecipeWidget } from "@/components/recipe-widget"; import { NewsWidget } from "@/components/news-widget"; import { SidebarWeatherWidget } from "@/components/weather-widget"; import { BreakingNewsWidget } from "@/components/breaking-news-widget"; +import PushPromptBanner from "@/components/push-prompt-banner"; import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from "react"; function useFocalPoints() { @@ -748,6 +749,7 @@ export default function Home() {