Add a prompt to encourage users to subscribe to push notifications
Introduce a new push notification prompt banner that appears on the homepage after a delay, encouraging users to subscribe to notifications. The banner will not appear if the user is already subscribed, has previously dismissed it, or if push notifications are not supported. Refactor existing push notification logic to be more modular and reusable across components. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: cf18a9e4-eed8-448c-9c3c-694f18134403 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ICRgny1 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
d5b8134b9d
commit
2c74322b1c
BIN
after-dismiss.png
Normal file
BIN
after-dismiss.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
after-reload.png
Normal file
BIN
after-reload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -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();
|
||||
const sub = await getExistingSubscription();
|
||||
setIsSubscribed(!!sub);
|
||||
} else {
|
||||
setIsSubscribed(false);
|
||||
}
|
||||
} catch {
|
||||
setIsSubscribed(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
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 {
|
||||
|
||||
90
client/src/components/push-prompt-banner.tsx
Normal file
90
client/src/components/push-prompt-banner.tsx
Normal file
@ -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 (
|
||||
<div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-6 md:max-w-sm z-50 animate-in slide-in-from-bottom-4 fade-in duration-500" data-testid="push-prompt-banner">
|
||||
<div className="bg-card border border-card-border rounded-xl shadow-2xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Bell className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-foreground text-sm" data-testid="text-push-prompt-title">
|
||||
Nichts mehr verpassen!
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Erhalten Sie Benachrichtigungen über neue Artikel und Videos von FOLX TV.
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={accept}
|
||||
className="text-xs h-8"
|
||||
data-testid="button-push-prompt-accept"
|
||||
>
|
||||
Ja, gerne!
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={dismiss}
|
||||
className="text-xs h-8 text-muted-foreground"
|
||||
data-testid="button-push-prompt-dismiss"
|
||||
>
|
||||
Nein, danke
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={dismiss}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
data-testid="button-push-prompt-close"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
</main>
|
||||
<Footer />
|
||||
<PushPromptBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
push-prompt-banner.png
Normal file
BIN
push-prompt-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Loading…
Reference in New Issue
Block a user