Add web push notification system for user engagement and updates

Implement a web push notification system, including service worker integration, user subscription management, and an admin interface for sending broadcast messages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f585829f-898b-492f-82a5-11f4a76c87fb
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:
sebastjanartic
2026-03-07 15:16:12 +00:00
parent 8e7dc999f0
commit f6478a7663
11 changed files with 423 additions and 1 deletions
+122
View File
@@ -0,0 +1,122 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import { Send, Bell, Users, ArrowLeft } from "lucide-react";
import { Link } from "wouter";
import { apiRequest } from "@/lib/queryClient";
export default function AdminPushPage() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [url, setUrl] = useState("");
const { toast } = useToast();
const { data: countData } = useQuery<{ count: number }>({
queryKey: ["/api/push/count"],
refetchInterval: 10000,
});
const sendMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest("POST", "/api/admin/push/send", { title, body, url: url || "/" });
return res.json();
},
onSuccess: (data: any) => {
toast({
title: "Push gesendet",
description: `Erfolgreich: ${data.sent}, Fehlgeschlagen: ${data.failed} von ${data.total} Abonnenten`,
});
setTitle("");
setBody("");
setUrl("");
},
onError: () => {
toast({ title: "Fehler", description: "Push-Nachricht konnte nicht gesendet werden.", variant: "destructive" });
},
});
return (
<div className="min-h-screen bg-background">
<div className="max-w-2xl mx-auto px-4 py-8">
<div className="flex items-center gap-3 mb-6">
<Link href="/">
<Button variant="ghost" size="icon" data-testid="button-back-home">
<ArrowLeft className="w-5 h-5" />
</Button>
</Link>
<Bell className="w-6 h-6 text-primary" />
<h1 className="text-2xl font-bold text-foreground" data-testid="text-admin-push-title">Push-Benachrichtigungen</h1>
</div>
<Card className="mb-6">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Users className="w-5 h-5" />
Abonnenten
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-primary" data-testid="text-subscriber-count">
{countData?.count ?? 0}
</p>
<p className="text-sm text-muted-foreground mt-1">aktive Push-Abonnenten</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Send className="w-5 h-5" />
Neue Benachrichtigung senden
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-foreground mb-1 block">Titel</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="z.B. Neuer Artikel auf FOLX TV"
data-testid="input-push-title"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground mb-1 block">Nachricht</label>
<Textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="z.B. Lesen Sie jetzt unseren neuesten Beitrag..."
rows={3}
data-testid="input-push-body"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground mb-1 block">Link (optional)</label>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="z.B. /article/mein-artikel-slug"
data-testid="input-push-url"
/>
<p className="text-xs text-muted-foreground mt-1">Leer lassen für Startseite</p>
</div>
<Button
onClick={() => sendMutation.mutate()}
disabled={!title.trim() || !body.trim() || sendMutation.isPending}
className="w-full"
data-testid="button-send-push"
>
{sendMutation.isPending ? "Wird gesendet..." : `An ${countData?.count ?? 0} Abonnenten senden`}
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}