644 lines
25 KiB
TypeScript
644 lines
25 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Card } from '@/components/ui/Card';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Badge } from '@/components/ui/Badge';
|
|
import {
|
|
Mail,
|
|
Users,
|
|
QrCode,
|
|
BarChart3,
|
|
TrendingUp,
|
|
Crown,
|
|
Activity,
|
|
Loader2,
|
|
Lock,
|
|
LogOut,
|
|
Zap,
|
|
Send,
|
|
CheckCircle2,
|
|
} from 'lucide-react';
|
|
|
|
interface AdminStats {
|
|
users: {
|
|
total: number;
|
|
premium: number;
|
|
newThisWeek: number;
|
|
newThisMonth: number;
|
|
recent: Array<{
|
|
email: string;
|
|
name: string | null;
|
|
plan: string;
|
|
createdAt: string;
|
|
}>;
|
|
};
|
|
qrCodes: {
|
|
total: number;
|
|
dynamic: number;
|
|
static: number;
|
|
active: number;
|
|
};
|
|
scans: {
|
|
total: number;
|
|
dynamicOnly: number;
|
|
avgPerDynamicQR: string;
|
|
};
|
|
newsletter: {
|
|
subscribers: number;
|
|
};
|
|
topQRCodes: Array<{
|
|
id: string;
|
|
title: string;
|
|
type: string;
|
|
scans: number;
|
|
owner: string;
|
|
createdAt: string;
|
|
}>;
|
|
}
|
|
|
|
export default function AdminDashboard() {
|
|
const router = useRouter();
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [isAuthenticating, setIsAuthenticating] = useState(true);
|
|
const [loginError, setLoginError] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
|
|
const [stats, setStats] = useState<AdminStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Newsletter management state
|
|
const [newsletterData, setNewsletterData] = useState<{
|
|
total: number;
|
|
recent: Array<{ email: string; createdAt: string }>;
|
|
} | null>(null);
|
|
const [sendingBroadcast, setSendingBroadcast] = useState(false);
|
|
const [broadcastResult, setBroadcastResult] = useState<{
|
|
success: boolean;
|
|
message: string;
|
|
} | null>(null);
|
|
|
|
useEffect(() => {
|
|
checkAuth();
|
|
}, []);
|
|
|
|
const checkAuth = async () => {
|
|
try {
|
|
const response = await fetch('/api/admin/stats');
|
|
if (response.ok) {
|
|
setIsAuthenticated(true);
|
|
const data = await response.json();
|
|
setStats(data);
|
|
setLoading(false);
|
|
// Also fetch newsletter data
|
|
fetchNewsletterData();
|
|
} else {
|
|
setIsAuthenticated(false);
|
|
}
|
|
} catch (error) {
|
|
setIsAuthenticated(false);
|
|
} finally {
|
|
setIsAuthenticating(false);
|
|
}
|
|
};
|
|
|
|
const fetchNewsletterData = async () => {
|
|
try {
|
|
const response = await fetch('/api/newsletter/broadcast');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setNewsletterData(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch newsletter data:', error);
|
|
}
|
|
};
|
|
|
|
const handleSendBroadcast = async () => {
|
|
if (!confirm(`Are you sure you want to send the AI Feature Launch email to all ${newsletterData?.total || 0} subscribers?`)) {
|
|
return;
|
|
}
|
|
|
|
setSendingBroadcast(true);
|
|
setBroadcastResult(null);
|
|
|
|
try {
|
|
const response = await fetch('/api/newsletter/broadcast', {
|
|
method: 'POST',
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
setBroadcastResult({
|
|
success: true,
|
|
message: data.message || `Successfully sent to ${data.sent} subscribers!`,
|
|
});
|
|
} else {
|
|
setBroadcastResult({
|
|
success: false,
|
|
message: data.error || 'Failed to send broadcast',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
setBroadcastResult({
|
|
success: false,
|
|
message: 'Network error. Please try again.',
|
|
});
|
|
} finally {
|
|
setSendingBroadcast(false);
|
|
}
|
|
};
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoginError('');
|
|
setIsAuthenticating(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/newsletter/admin-login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
|
|
if (response.ok) {
|
|
setIsAuthenticated(true);
|
|
await checkAuth();
|
|
} else {
|
|
const data = await response.json();
|
|
setLoginError(data.error || 'Invalid credentials');
|
|
}
|
|
} catch (error) {
|
|
setLoginError('Login failed. Please try again.');
|
|
} finally {
|
|
setIsAuthenticating(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
router.push('/');
|
|
};
|
|
|
|
// Login Screen
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 px-4">
|
|
<Card className="w-full max-w-md p-8">
|
|
<div className="text-center mb-6">
|
|
<div className="w-16 h-16 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<Lock className="w-8 h-8 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<h1 className="text-2xl font-bold mb-2">Admin Dashboard</h1>
|
|
<p className="text-muted-foreground text-sm">
|
|
Sign in to access admin panel
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleLogin} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Email</label>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="admin@example.com"
|
|
required
|
|
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Password</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="••••••••"
|
|
required
|
|
className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{loginError && (
|
|
<p className="text-sm text-red-600 dark:text-red-400">{loginError}</p>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={isAuthenticating}
|
|
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
|
|
>
|
|
{isAuthenticating ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
Signing in...
|
|
</>
|
|
) : (
|
|
'Sign In'
|
|
)}
|
|
</Button>
|
|
</form>
|
|
|
|
<div className="mt-6 pt-6 border-t text-center">
|
|
<p className="text-xs text-muted-foreground">
|
|
Admin credentials required
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Loading
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Admin Dashboard
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-purple-50/30 to-pink-50/30 dark:from-purple-950/10 dark:to-pink-950/10">
|
|
<div className="container mx-auto px-4 py-8 max-w-7xl">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h1 className="text-3xl font-bold mb-2">Admin Dashboard</h1>
|
|
<p className="text-muted-foreground">
|
|
Platform overview and statistics
|
|
</p>
|
|
</div>
|
|
<Button
|
|
onClick={handleLogout}
|
|
variant="outline"
|
|
className="flex items-center gap-2"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Logout
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Main Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
{/* All Time Users */}
|
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
|
|
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
|
|
All Time
|
|
</Badge>
|
|
</div>
|
|
<h3 className="text-3xl font-bold mb-1">{stats?.users.total || 0}</h3>
|
|
<p className="text-sm text-muted-foreground">Total Users</p>
|
|
<div className="mt-3 pt-3 border-t space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">This Month</span>
|
|
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
|
|
+{stats?.users.newThisMonth || 0}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">This Week</span>
|
|
<span className="text-sm font-semibold text-green-600 dark:text-green-400">
|
|
+{stats?.users.newThisWeek || 0}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Dynamic QR Codes */}
|
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900/20 rounded-lg flex items-center justify-center">
|
|
<QrCode className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
|
Dynamic
|
|
</Badge>
|
|
</div>
|
|
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.dynamic || 0}</h3>
|
|
<p className="text-sm text-muted-foreground">Dynamic QR Codes</p>
|
|
<div className="mt-3 pt-3 border-t flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">Static</span>
|
|
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Total Scans */}
|
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
|
|
<BarChart3 className="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
|
All Time
|
|
</Badge>
|
|
</div>
|
|
<h3 className="text-3xl font-bold mb-1">
|
|
{stats?.scans.dynamicOnly.toLocaleString() || 0}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">Dynamic QR Scans</p>
|
|
<div className="mt-3 pt-3 border-t flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">Avg per QR</span>
|
|
<span className="text-sm font-semibold">{stats?.scans.avgPerDynamicQR || 0}</span>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Total QR Codes */}
|
|
<Card className="p-6 hover:shadow-lg transition-shadow">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
|
|
<QrCode className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
|
|
All Time
|
|
</Badge>
|
|
</div>
|
|
<h3 className="text-3xl font-bold mb-1">{stats?.qrCodes.total || 0}</h3>
|
|
<p className="text-sm text-muted-foreground">Total QR Codes</p>
|
|
<div className="mt-3 pt-3 border-t space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">Dynamic</span>
|
|
<span className="text-sm font-semibold">{stats?.qrCodes.dynamic || 0}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">Static</span>
|
|
<span className="text-sm font-semibold">{stats?.qrCodes.static || 0}</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Secondary Stats Row */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
{/* Total All Scans */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/20 rounded-lg flex items-center justify-center">
|
|
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-2xl font-bold">
|
|
{stats?.scans.total.toLocaleString() || 0}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">Total All Scans</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Total QR Codes */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-pink-100 dark:bg-pink-900/20 rounded-lg flex items-center justify-center">
|
|
<QrCode className="w-6 h-6 text-pink-600 dark:text-pink-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-2xl font-bold">{stats?.qrCodes.total || 0}</h3>
|
|
<p className="text-sm text-muted-foreground">Total QR Codes</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Premium Users */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/20 rounded-lg flex items-center justify-center">
|
|
<Crown className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-2xl font-bold">{stats?.users.premium || 0}</h3>
|
|
<p className="text-sm text-muted-foreground">Premium Users</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Bottom Grid */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Top QR Codes */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
|
|
<TrendingUp className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">Top QR Codes</h3>
|
|
<p className="text-xs text-muted-foreground">Most scanned</p>
|
|
</div>
|
|
</div>
|
|
|
|
{stats?.topQRCodes && stats.topQRCodes.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{stats.topQRCodes.map((qr, index) => (
|
|
<div
|
|
key={qr.id}
|
|
className="flex items-center justify-between py-3 border-b border-border last:border-0"
|
|
>
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
<span className="text-white text-sm font-bold">
|
|
#{index + 1}
|
|
</span>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium truncate">{qr.title}</p>
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
{qr.owner}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right flex-shrink-0 ml-4">
|
|
<p className="text-lg font-bold">{qr.scans.toLocaleString()}</p>
|
|
<p className="text-xs text-muted-foreground">scans</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No QR codes yet</p>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Recent Users */}
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
|
|
<Users className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-lg">Recent Users</h3>
|
|
<p className="text-xs text-muted-foreground">Latest signups</p>
|
|
</div>
|
|
</div>
|
|
|
|
{stats?.users.recent && stats.users.recent.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{stats.users.recent.map((user, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between py-3 border-b border-border last:border-0"
|
|
>
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<span className="text-white text-xs font-bold">
|
|
{(user.name || user.email).charAt(0).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium truncate">
|
|
{user.name || user.email}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
{new Date(user.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Badge
|
|
className={
|
|
user.plan === 'FREE'
|
|
? 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300'
|
|
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
|
}
|
|
>
|
|
{user.plan === 'PRO' && <Crown className="w-3 h-3 mr-1" />}
|
|
{user.plan}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No users yet</p>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Newsletter Management Section */}
|
|
<div className="mt-8">
|
|
<Card className="p-6">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/30 dark:to-pink-900/30 rounded-lg flex items-center justify-center">
|
|
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold text-lg">Newsletter Management</h3>
|
|
<p className="text-xs text-muted-foreground">Manage AI feature launch notifications</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="text-2xl font-bold">{newsletterData?.total || 0}</span>
|
|
<p className="text-xs text-muted-foreground">Total Subscribers</p>
|
|
</div>
|
|
<Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
|
Active
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Broadcast Section */}
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 rounded-xl mb-6">
|
|
<div className="flex items-start gap-3 mb-3">
|
|
<Send className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5" />
|
|
<div>
|
|
<h4 className="font-medium">Broadcast AI Feature Launch</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
Send the AI feature launch announcement to all {newsletterData?.total || 0} subscribers.
|
|
This will inform them that the features are now available.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Resend Free Tier Warning */}
|
|
{(newsletterData?.total || 0) > 100 && (
|
|
<div className="p-3 rounded-lg mb-3 bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 flex items-start gap-2">
|
|
<Activity className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
|
<div className="text-sm">
|
|
<strong>Warning: Resend Free Limit</strong>
|
|
<p>You have more than 100 subscribers. The Resend Free Tier only allows 100 emails per day. Sending this broadcast might fail for some users or block your account.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{broadcastResult && (
|
|
<div className={`p-3 rounded-lg mb-3 flex items-center gap-2 ${broadcastResult.success
|
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
|
}`}>
|
|
{broadcastResult.success && <CheckCircle2 className="w-4 h-4" />}
|
|
<span className="text-sm">{broadcastResult.message}</span>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
onClick={handleSendBroadcast}
|
|
disabled={sendingBroadcast || (newsletterData?.total || 0) === 0 || (newsletterData?.total || 0) > 100}
|
|
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
|
|
>
|
|
{sendingBroadcast ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
Sending...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Mail className="w-4 h-4 mr-2" />
|
|
Send Launch Notification to All
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Recent Subscribers */}
|
|
<div>
|
|
<h4 className="font-medium mb-3">Recent Subscribers</h4>
|
|
{newsletterData?.recent && newsletterData.recent.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{newsletterData.recent.map((subscriber, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between py-2 border-b border-border last:border-0"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Mail className="w-4 h-4 text-muted-foreground" />
|
|
<span className="text-sm">{subscriber.email}</span>
|
|
</div>
|
|
<span className="text-xs text-muted-foreground">
|
|
{new Date(subscriber.createdAt).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No subscribers yet</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tip */}
|
|
<div className="mt-4 pt-4 border-t">
|
|
<p className="text-xs text-muted-foreground">
|
|
💡 Tip: View all subscribers in{' '}
|
|
<a
|
|
href="http://localhost:5555"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-purple-600 dark:text-purple-400 hover:underline"
|
|
>
|
|
Prisma Studio
|
|
</a>
|
|
{' '}(NewsletterSubscription table)
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|