import { IconDownload as Download, IconLoader2 as Loader2, IconMail as Mail, IconSearch as Search, IconSend as Send, IconTrash as Trash2, IconUsers as Users, } from '@tabler/icons-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { toast } from 'sonner'; import { PageHeader } from '@/components/PageHeader'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { apiFetch, apiJson } from '@/lib/api'; import { ContentPanel } from '../../components/ContentPanel'; interface Subscriber { id: string; email: string; name: string | null; status: string; subscribed_at: string | null; created_at: string; } interface Stats { total: number; active: number; pending: number; unsubscribed: number; } const subscribersKey = ['admin', 'newsletter-subscribers']; const statsKey = ['admin', 'newsletter-stats']; function StatusBadge({ status }: { status: string }) { const variants: Record = { active: 'default', pending: 'secondary', unsubscribed: 'outline', }; return {status}; } export default function AdminNewsletter() { const queryClient = useQueryClient(); const [search, setSearch] = useState(''); const [composeOpen, setComposeOpen] = useState(false); const [composeSubject, setComposeSubject] = useState(''); const [composeBody, setComposeBody] = useState(''); const { data: stats } = useQuery({ queryKey: statsKey, queryFn: () => apiJson('/api/v1/admin/newsletter/stats', {}, 'Failed to fetch stats'), }); const { data, isLoading } = useQuery<{ subscribers: Subscriber[]; total: number }>({ queryKey: [...subscribersKey, search], queryFn: () => { const params = new URLSearchParams({ limit: '50' }); if (search) params.set('search', search); return apiJson<{ subscribers: Subscriber[]; total: number }>( `/api/v1/admin/newsletter?${params}`, {}, 'Failed to fetch subscribers' ); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => apiJson(`/api/v1/admin/newsletter/${id}`, { method: 'DELETE' }, 'Failed to delete'), onSuccess: () => { queryClient.invalidateQueries({ queryKey: subscribersKey }); queryClient.invalidateQueries({ queryKey: statsKey }); toast.success('Subscriber removed'); }, }); const sendMutation = useMutation({ mutationFn: () => apiJson<{ sent: number; failed: number }>( '/api/v1/admin/newsletter/send', { method: 'POST', body: { subject: composeSubject, html: composeBody.replace(/\n/g, '
'), }, }, 'Failed to send' ), onSuccess: (data: { sent: number; failed: number }) => { setComposeOpen(false); setComposeSubject(''); setComposeBody(''); toast.success(`Newsletter sent to ${data.sent} subscribers`); if (data.failed > 0) { toast.error(`${data.failed} emails failed to send`); } }, onError: (err) => { toast.error(err.message); }, }); const handleExport = async () => { const response = await apiFetch('/api/v1/admin/newsletter/export'); if (!response.ok) { toast.error('Failed to export'); return; } const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'newsletter-subscribers.csv'; a.click(); URL.revokeObjectURL(url); }; const subscribers = data?.subscribers ?? []; return ( <>
{/* Stats */} {stats && (
{[ { label: 'Total', value: stats.total }, { label: 'Active', value: stats.active }, { label: 'Pending', value: stats.pending }, { label: 'Unsubscribed', value: stats.unsubscribed }, ].map((stat) => (
{stat.value}
{stat.label}
))}
)} {/* Actions */}
{/* Subscribers */} Subscribers {data?.total ?? 0} total subscribers
setSearch(e.target.value)} className="pl-9" />
{isLoading ? (
) : subscribers.length === 0 ? (

No subscribers yet.

) : (
{subscribers.map((sub) => (
{sub.email}
{sub.name && (
{sub.name}
)}
{new Date(sub.created_at).toLocaleDateString()}
))}
)}
{/* Compose dialog */} Compose Newsletter This will be sent to {stats?.active ?? 0} active subscribers. Each email includes an unsubscribe link.
setComposeSubject(e.target.value)} placeholder="Newsletter subject..." />