diff --git a/src/app/(marketing)/newsletter/page.tsx b/src/app/(marketing)/newsletter/page.tsx index 2ea562e..04d842c 100644 --- a/src/app/(marketing)/newsletter/page.tsx +++ b/src/app/(marketing)/newsletter/page.tsx @@ -5,19 +5,60 @@ 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, Send, CheckCircle, AlertCircle, Loader2, Lock, LogOut } from 'lucide-react'; +import { + Mail, + Users, + QrCode, + BarChart3, + TrendingUp, + Crown, + Activity, + Loader2, + Lock, + LogOut, + Zap, + Send, + CheckCircle2, +} from 'lucide-react'; -interface Subscriber { - email: string; - createdAt: string; +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; + }>; } -interface BroadcastInfo { - total: number; - recent: Subscriber[]; -} - -export default function NewsletterPage() { +export default function AdminDashboard() { const router = useRouter(); const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(true); @@ -25,14 +66,18 @@ export default function NewsletterPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [info, setInfo] = useState(null); + const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); - const [broadcasting, setBroadcasting] = useState(false); - const [result, setResult] = useState<{ + + // 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; - sent?: number; - failed?: number; } | null>(null); useEffect(() => { @@ -41,12 +86,14 @@ export default function NewsletterPage() { const checkAuth = async () => { try { - const response = await fetch('/api/newsletter/broadcast'); + const response = await fetch('/api/admin/stats'); if (response.ok) { setIsAuthenticated(true); const data = await response.json(); - setInfo(data); + setStats(data); setLoading(false); + // Also fetch newsletter data + fetchNewsletterData(); } else { setIsAuthenticated(false); } @@ -57,6 +104,54 @@ export default function NewsletterPage() { } }; + 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(''); @@ -90,53 +185,6 @@ export default function NewsletterPage() { router.push('/'); }; - const fetchSubscriberInfo = async () => { - try { - const response = await fetch('/api/newsletter/broadcast'); - if (response.ok) { - const data = await response.json(); - setInfo(data); - } - } catch (error) { - console.error('Failed to fetch subscriber info:', error); - } - }; - - const handleBroadcast = async () => { - if (!confirm(`Are you sure you want to send the AI feature launch email to ${info?.total} subscribers?`)) { - return; - } - - setBroadcasting(true); - setResult(null); - - try { - const response = await fetch('/api/newsletter/broadcast', { - method: 'POST', - }); - - const data = await response.json(); - - setResult({ - success: response.ok, - message: data.message || data.error, - sent: data.sent, - failed: data.failed, - }); - - if (response.ok) { - await fetchSubscriberInfo(); - } - } catch (error) { - setResult({ - success: false, - message: 'Failed to send broadcast. Please try again.', - }); - } finally { - setBroadcasting(false); - } - }; - // Login Screen if (!isAuthenticated) { return ( @@ -146,9 +194,9 @@ export default function NewsletterPage() {
-

Newsletter Admin

+

Admin Dashboard

- Sign in to manage subscribers + Sign in to access admin panel

@@ -159,7 +207,7 @@ export default function NewsletterPage() { type="email" value={email} onChange={(e) => setEmail(e.target.value)} - placeholder="Email" + 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" /> @@ -171,7 +219,7 @@ export default function NewsletterPage() { type="password" value={password} onChange={(e) => setPassword(e.target.value)} - placeholder="Password" + 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" /> @@ -219,12 +267,13 @@ export default function NewsletterPage() { // Admin Dashboard return (
-
+
+ {/* Header */}
-

Newsletter Management

+

Admin Dashboard

- Manage AI feature launch notifications + Platform overview and statistics

- {/* Stats Card */} - -
-
-
- + {/* Main Stats Grid */} +
+ {/* All Time Users */} + +
+
+
-
-

{info?.total || 0}

-

Total Subscribers

+ + All Time + +
+

{stats?.users.total || 0}

+

Total Users

+
+
+ This Month + + +{stats?.users.newThisMonth || 0} +
-
- - Active - -
- - {/* Broadcast Button */} -
-
-

- - Broadcast AI Feature Launch -

-

- Send the AI feature launch announcement to all {info?.total} subscribers. - This will inform them that the features are now available. -

-
- - -
-
- - {/* Result Message */} - {result && ( - -
- {result.success ? ( - - ) : ( - - )} -
-

- {result.message} -

- {result.sent !== undefined && ( -

- Sent: {result.sent} | Failed: {result.failed} -

- )} +
+ This Week + + +{stats?.users.newThisWeek || 0} +
- )} - {/* Recent Subscribers */} - -

Recent Subscribers

- {info?.recent && info.recent.length > 0 ? ( -
- {info.recent.map((subscriber, index) => ( -
-
- - {subscriber.email} -
- - {new Date(subscriber.createdAt).toLocaleDateString()} - -
- ))} + {/* Dynamic QR Codes */} + +
+
+ +
+ + Dynamic +
- ) : ( -

No subscribers yet

- )} +

{stats?.qrCodes.dynamic || 0}

+

Dynamic QR Codes

+
+ Static + {stats?.qrCodes.static || 0} +
+
- + + {/* Secondary Stats Row */} +
+ {/* Total All Scans */} + +
+
+ +
+
+

+ {stats?.scans.total.toLocaleString() || 0} +

+

Total All Scans

+
+
+
+ + {/* Total QR Codes */} + +
+
+ +
+
+

{stats?.qrCodes.total || 0}

+

Total QR Codes

+
+
+
+ + {/* Premium Users */} + +
+
+ +
+
+

{stats?.users.premium || 0}

+

Premium Users

+
+
+
+
+ + {/* Bottom Grid */} +
+ {/* Top QR Codes */} + +
+
+ +
+
+

Top QR Codes

+

Most scanned

+
+
+ + {stats?.topQRCodes && stats.topQRCodes.length > 0 ? ( +
+ {stats.topQRCodes.map((qr, index) => ( +
+
+
+ + #{index + 1} + +
+
+

{qr.title}

+

+ {qr.owner} +

+
+
+
+

{qr.scans.toLocaleString()}

+

scans

+
+
+ ))} +
+ ) : ( +

No QR codes yet

+ )} +
+ + {/* Recent Users */} + +
+
+ +
+
+

Recent Users

+

Latest signups

+
+
+ + {stats?.users.recent && stats.users.recent.length > 0 ? ( +
+ {stats.users.recent.map((user, index) => ( +
+
+
+ + {(user.name || user.email).charAt(0).toUpperCase()} + +
+
+

+ {user.name || user.email} +

+

+ {new Date(user.createdAt).toLocaleDateString()} +

+
+
+ + {user.plan === 'PRO' && } + {user.plan} + +
+ ))} +
+ ) : ( +

No users yet

+ )} +
+
+ + {/* Newsletter Management Section */} +
+ +
+
+ +
+
+

Newsletter Management

+

Manage AI feature launch notifications

+
+
+ {newsletterData?.total || 0} +

Total Subscribers

+
+ + Active + +
+ + {/* Broadcast Section */} +
- + {sendingBroadcast ? ( + <> + + Sending... + + ) : ( + <> + + Send Launch Notification to All + + )} + +
+ + {/* Recent Subscribers */} +
+

Recent Subscribers

+ {newsletterData?.recent && newsletterData.recent.length > 0 ? ( +
+ {newsletterData.recent.map((subscriber, index) => ( +
+
+ + {subscriber.email} +
+ + {new Date(subscriber.createdAt).toLocaleDateString()} + +
+ ))} +
+ ) : ( +

No subscribers yet

+ )} +
+ + {/* Tip */} +
+

+ 💡 Tip: View all subscribers in{' '} + + Prisma Studio + + {' '}(NewsletterSubscription table) +

+
+ +
); diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts new file mode 100644 index 0000000..13b2434 --- /dev/null +++ b/src/app/api/admin/stats/route.ts @@ -0,0 +1,216 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { db } from '@/lib/db'; + +export async function GET(request: NextRequest) { + try { + // Check newsletter-admin cookie authentication + const cookieStore = cookies(); + const adminCookie = cookieStore.get('newsletter-admin'); + + if (!adminCookie || adminCookie.value !== 'authenticated') { + return NextResponse.json( + { error: 'Unauthorized - Admin login required' }, + { status: 401 } + ); + } + + // Get 30 days ago date + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + // Get 7 days ago date + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + // Get start of current month + const startOfMonth = new Date(); + startOfMonth.setDate(1); + startOfMonth.setHours(0, 0, 0, 0); + + // Fetch all statistics in parallel + const [ + totalUsers, + premiumUsers, + newUsersThisWeek, + newUsersThisMonth, + totalQRCodes, + dynamicQRCodes, + staticQRCodes, + totalScans, + dynamicQRCodesWithScans, + activeQRCodes, + newsletterSubscribers, + ] = await Promise.all([ + // Total users + db.user.count(), + + // Premium users (PRO or BUSINESS) + db.user.count({ + where: { + plan: { + in: ['PRO', 'BUSINESS'], + }, + }, + }), + + // New users this week + db.user.count({ + where: { + createdAt: { + gte: sevenDaysAgo, + }, + }, + }), + + // New users this month + db.user.count({ + where: { + createdAt: { + gte: startOfMonth, + }, + }, + }), + + // Total QR codes + db.qRCode.count(), + + // Dynamic QR codes + db.qRCode.count({ + where: { + type: 'DYNAMIC', + }, + }), + + // Static QR codes + db.qRCode.count({ + where: { + type: 'STATIC', + }, + }), + + // Total scans + db.qRScan.count(), + + // Get all dynamic QR codes with their scan counts + db.qRCode.findMany({ + where: { + type: 'DYNAMIC', + }, + include: { + _count: { + select: { + scans: true, + }, + }, + }, + }), + + // Active QR codes (scanned in last 30 days) + db.qRCode.findMany({ + where: { + scans: { + some: { + ts: { + gte: thirtyDaysAgo, + }, + }, + }, + }, + distinct: ['id'], + }), + + // Newsletter subscribers + db.newsletterSubscription.count({ + where: { + status: 'subscribed', + }, + }), + ]); + + // Calculate dynamic QR scans + const dynamicQRScans = dynamicQRCodesWithScans.reduce( + (total, qr) => total + qr._count.scans, + 0 + ); + + // Calculate average scans per dynamic QR + const avgScansPerDynamicQR = + dynamicQRCodes > 0 ? (dynamicQRScans / dynamicQRCodes).toFixed(1) : '0'; + + // Get top 5 most scanned QR codes + const topQRCodes = await db.qRCode.findMany({ + take: 5, + include: { + _count: { + select: { + scans: true, + }, + }, + user: { + select: { + email: true, + name: true, + }, + }, + }, + orderBy: { + scans: { + _count: 'desc', + }, + }, + }); + + // Get recent users + const recentUsers = await db.user.findMany({ + take: 5, + orderBy: { + createdAt: 'desc', + }, + select: { + email: true, + name: true, + plan: true, + createdAt: true, + }, + }); + + return NextResponse.json({ + users: { + total: totalUsers, + premium: premiumUsers, + newThisWeek: newUsersThisWeek, + newThisMonth: newUsersThisMonth, + recent: recentUsers, + }, + qrCodes: { + total: totalQRCodes, + dynamic: dynamicQRCodes, + static: staticQRCodes, + active: activeQRCodes.length, + }, + scans: { + total: totalScans, + dynamicOnly: dynamicQRScans, + avgPerDynamicQR: avgScansPerDynamicQR, + }, + newsletter: { + subscribers: newsletterSubscribers, + }, + topQRCodes: topQRCodes.map((qr) => ({ + id: qr.id, + title: qr.title, + type: qr.type, + scans: qr._count.scans, + owner: qr.user.name || qr.user.email, + createdAt: qr.createdAt, + })), + }); + } catch (error) { + console.error('Error fetching admin stats:', error); + return NextResponse.json( + { error: 'Failed to fetch statistics' }, + { status: 500 } + ); + } +}