167 lines
6.4 KiB
TypeScript
167 lines
6.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { usePushStats, useSendPushNotification } from '@/queries/push';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Bell, Send, Users, AlertCircle, CheckCircle } from 'lucide-react';
|
|
|
|
export function PushNotificationManager() {
|
|
const [title, setTitle] = useState('');
|
|
const [body, setBody] = useState('');
|
|
const [url, setUrl] = useState('');
|
|
const [showSuccess, setShowSuccess] = useState(false);
|
|
const [result, setResult] = useState<{ sent: number; failed: number } | null>(
|
|
null,
|
|
);
|
|
|
|
const { data: stats, isLoading: loadingStats } = usePushStats();
|
|
const sendMutation = useSendPushNotification();
|
|
|
|
const handleSendTest = async () => {
|
|
if (!title.trim() || !body.trim()) return;
|
|
|
|
try {
|
|
const result = await sendMutation.mutateAsync({
|
|
title: title.trim(),
|
|
body: body.trim(),
|
|
url: url.trim() || undefined,
|
|
});
|
|
setResult(result);
|
|
setShowSuccess(true);
|
|
setTimeout(() => setShowSuccess(false), 5000);
|
|
} catch (error) {
|
|
console.error('Failed to send notification:', error);
|
|
}
|
|
};
|
|
|
|
const isValid = title.trim().length > 0 && body.trim().length > 0;
|
|
|
|
return (
|
|
<div className="border-brutal bg-card p-6">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<Bell className="h-6 w-6 text-accent" />
|
|
<h2 className="text-2xl font-display uppercase">Push Notifications</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-1">
|
|
<div className="border-brutal-sm bg-background p-5 h-full">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Users className="h-5 w-5 text-muted-foreground" />
|
|
<span className="font-body text-sm uppercase tracking-wider text-muted-foreground">
|
|
Subscribers
|
|
</span>
|
|
</div>
|
|
{loadingStats ? (
|
|
<div className="text-4xl font-display animate-pulse">...</div>
|
|
) : (
|
|
<div className="text-5xl font-display">
|
|
{stats?.totalSubscribers ?? 0}
|
|
</div>
|
|
)}
|
|
<p className="text-xs font-body text-muted-foreground mt-2">
|
|
Active push subscribers
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="lg:col-span-2">
|
|
<div className="border-brutal-sm bg-background p-5">
|
|
<h3 className="font-body text-sm uppercase tracking-wider text-muted-foreground mb-4">
|
|
Send Notification
|
|
</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block font-body text-xs uppercase tracking-wider text-muted-foreground mb-1">
|
|
Title
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="Enter notification title..."
|
|
className="w-full border-2 border-foreground bg-background px-3 py-2 font-body text-sm focus:border-accent focus:outline-none"
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-body text-xs uppercase tracking-wider text-muted-foreground mb-1">
|
|
Body
|
|
</label>
|
|
<textarea
|
|
value={body}
|
|
onChange={(e) => setBody(e.target.value)}
|
|
placeholder="Enter notification message..."
|
|
className="w-full border-2 border-foreground bg-background px-3 py-2 font-body text-sm focus:border-accent focus:outline-none resize-none"
|
|
rows={3}
|
|
maxLength={150}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-body text-xs uppercase tracking-wider text-muted-foreground mb-1">
|
|
URL (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
placeholder="/article/slug or /live-blogs/slug"
|
|
className="w-full border-2 border-foreground bg-background px-3 py-2 font-body text-sm focus:border-accent focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-4 pt-2">
|
|
<div className="text-xs font-body text-muted-foreground">
|
|
{title.length}/50 chars • {body.length}/150 chars
|
|
</div>
|
|
|
|
<Button
|
|
variant="brutalAccent"
|
|
onClick={handleSendTest}
|
|
disabled={!isValid || sendMutation.isPending}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
{sendMutation.isPending ? 'Sending...' : 'Send to All'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{showSuccess && result && (
|
|
<div className="mt-4 p-3 border-2 border-green-500 bg-green-500/10 flex items-start gap-2">
|
|
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p className="font-body text-sm font-medium">
|
|
Notification sent!
|
|
</p>
|
|
<p className="font-body text-xs text-muted-foreground">
|
|
Delivered: {result.sent} • Failed: {result.failed}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{sendMutation.isError && (
|
|
<div className="mt-4 p-3 border-2 border-red-500 bg-red-500/10 flex items-center gap-2">
|
|
<AlertCircle className="h-5 w-5 text-red-500" />
|
|
<p className="font-body text-sm">
|
|
Failed to send notification. Please try again.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 p-4 border-2 border-foreground/20 bg-foreground/5">
|
|
<p className="font-body text-xs text-muted-foreground">
|
|
<strong>Note:</strong> Notifications are sent to all subscribers with
|
|
push enabled. Use sparingly to avoid spam. Articles and live blog
|
|
updates are automatically pushed when published.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|