![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/pages/admin/ |
import React, { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import LayoutWithSidebar from '../../components/LayoutWithSidebar';
import { motion, AnimatePresence } from 'framer-motion';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { canAccessNewsletter } from '@/lib/auth-utils';
interface NewsletterSubscription {
id: string;
email: string;
language: string;
source: string;
subscriptionDate: string;
isActive: boolean;
confirmedAt?: string;
unsubscribedAt?: string;
tags?: string;
createdAt: string;
updatedAt: string;
}
interface NewsletterStats {
total: number;
active: number;
inactive: number;
byLanguage: Record<string, number>;
bySource: Record<string, number>;
}
interface Analytics {
overview: {
totalSubscribers: number;
activeSubscribers: number;
newSubscribers: number;
unsubscribers: number;
totalCampaigns: number;
totalEvents: number;
growthRate: number;
};
performance: {
totalSent: number;
totalOpened: number;
totalClicked: number;
totalBounced: number;
totalUnsubscribed: number;
openRate: number;
clickRate: number;
bounceRate: number;
unsubscribeRate: number;
};
topCampaigns: Array<{
id: string;
name: string;
subject: string;
totalSent: number;
totalOpened: number;
totalClicked: number;
openRate: number;
clickRate: number;
sentAt: string;
}>;
}
const AdminNewsletter: React.FC = () => {
const [language, setLanguage] = useState<'fr' | 'en'>('fr');
const handleLanguageToggle = () => {
const newLang = language === 'fr' ? 'en' : 'fr';
setLanguage(newLang);
};
const { data: session, status } = useSession();
const router = useRouter();
const [subscriptions, setSubscriptions] = useState<NewsletterSubscription[]>([]);
const [analytics, setAnalytics] = useState<Analytics | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
const [filters, setFilters] = useState({
status: 'all',
language: 'all',
source: 'all',
search: '',
page: 1,
limit: 20
});
useEffect(() => {
if (status === 'authenticated' && canAccessNewsletter(session)) {
fetchData();
}
}, [session, status, filters]);
const fetchData = async () => {
try {
setLoading(true);
// Fetch subscriptions and analytics in parallel
const [subscriptionsRes, analyticsRes] = await Promise.all([
fetch(`/api/admin/newsletter?${new URLSearchParams({
page: filters.page.toString(),
limit: filters.limit.toString(),
status: filters.status,
language: filters.language,
source: filters.source,
search: filters.search
})}`),
fetch('/api/admin/newsletter/analytics?timeframe=30d')
]);
if (subscriptionsRes.ok) {
const data = await subscriptionsRes.json();
setSubscriptions(data.subscriptions);
}
if (analyticsRes.ok) {
const data = await analyticsRes.json();
setAnalytics(data);
}
} catch (error) {
toast.error('Error fetching newsletter data');
} finally {
setLoading(false);
}
};
const handleExport = async (format: 'csv' | 'json' = 'csv') => {
try {
const response = await fetch(`/api/admin/newsletter/export?format=${format}&status=${filters.status}`);
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `newsletter-subscriptions-${new Date().toISOString().split('T')[0]}.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
toast.success(`Newsletter data exported as ${format.toUpperCase()}`);
} else {
toast.error('Export failed');
}
} catch (error) {
toast.error('Export error');
}
};
const handleBulkAction = async (action: 'activate' | 'deactivate' | 'delete', selectedIds: string[]) => {
if (selectedIds.length === 0) {
toast.error('No subscriptions selected');
return;
}
if (action === 'delete' && !confirm(`Are you sure you want to delete ${selectedIds.length} subscription(s)?`)) {
return;
}
try {
const response = await fetch('/api/admin/newsletter/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, ids: selectedIds })
});
const data = await response.json();
if (response.ok) {
toast.success(`Successfully ${action}d ${selectedIds.length} subscription(s)`);
fetchData();
} else {
toast.error(data.message || 'Bulk action failed');
}
} catch (error) {
toast.error('Bulk action error');
}
};
if (status === 'loading') {
return <div className="flex justify-center items-center min-h-screen">Loading...</div>;
}
if (!session || !canAccessNewsletter(session)) {
router.push('/');
return null;
}
return (
<LayoutWithSidebar>
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Newsletter Management
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
Comprehensive newsletter management with advanced features
</p>
</div>
<div className="flex space-x-3">
<Link href="/admin/newsletter/templates">
<button className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg font-medium">
📧 Templates
</button>
</Link>
<button
onClick={() => handleExport('csv')}
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg font-medium"
>
📊 Export
</button>
<button
onClick={fetchData}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium"
>
🔄 Refresh
</button>
</div>
</div>
{/* Tab Navigation */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
<div className="flex border-b border-gray-200 dark:border-gray-700">
{[
{ key: 'overview', label: '📊 Overview', icon: '📊' },
{ key: 'subscribers', label: '👥 Subscribers', icon: '👥' },
{ key: 'campaigns', label: '📧 Campaigns', icon: '📧' },
{ key: 'analytics', label: '📈 Analytics', icon: '📈' },
{ key: 'segments', label: '🎯 Segments', icon: '🎯' }
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
activeTab === tab.key
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Overview Tab */}
{activeTab === 'overview' && analytics && (
<div className="space-y-6">
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Subscribers</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{analytics.overview.totalSubscribers.toLocaleString()}
</p>
</div>
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<span className="text-2xl">👥</span>
</div>
</div>
<div className="mt-4">
<div className={`flex items-center text-sm ${
analytics.overview.growthRate >= 0 ? 'text-green-600' : 'text-red-600'
}`}>
<span className="mr-1">{analytics.overview.growthRate >= 0 ? '↗️' : '↘️'}</span>
{Math.abs(analytics.overview.growthRate).toFixed(1)}% growth rate
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Active Subscribers</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{analytics.overview.activeSubscribers.toLocaleString()}
</p>
</div>
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<span className="text-2xl">✅</span>
</div>
</div>
<div className="mt-4">
<div className="flex items-center text-sm text-gray-600 dark:text-gray-400">
{((analytics.overview.activeSubscribers / analytics.overview.totalSubscribers) * 100).toFixed(1)}% of total
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Open Rate</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{analytics.performance.openRate.toFixed(1)}%
</p>
</div>
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<span className="text-2xl">📧</span>
</div>
</div>
<div className="mt-4">
<div className="flex items-center text-sm text-gray-600 dark:text-gray-400">
{analytics.performance.totalOpened.toLocaleString()} of {analytics.performance.totalSent.toLocaleString()} sent
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Click Rate</p>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{analytics.performance.clickRate.toFixed(1)}%
</p>
</div>
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center">
<span className="text-2xl">🖱️</span>
</div>
</div>
<div className="mt-4">
<div className="flex items-center text-sm text-gray-600 dark:text-gray-400">
{analytics.performance.totalClicked.toLocaleString()} clicks
</div>
</div>
</motion.div>
</div>
{/* Top Campaigns */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">🏆 Top Performing Campaigns</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mt-1">Best campaigns by open rate in the last 30 days</p>
</div>
<div className="p-6">
{analytics.topCampaigns.length > 0 ? (
<div className="space-y-4">
{analytics.topCampaigns.map((campaign, index) => (
<div key={campaign.id} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div className="flex items-center space-x-4">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white font-bold ${
index === 0 ? 'bg-yellow-500' : index === 1 ? 'bg-gray-400' : index === 2 ? 'bg-orange-500' : 'bg-blue-500'
}`}>
{index + 1}
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">{campaign.name}</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 truncate max-w-xs">{campaign.subject}</p>
</div>
</div>
<div className="text-right">
<div className="flex items-center space-x-4 text-sm">
<div>
<p className="text-gray-600 dark:text-gray-400">Open Rate</p>
<p className="font-bold text-gray-900 dark:text-white">{campaign.openRate}%</p>
</div>
<div>
<p className="text-gray-600 dark:text-gray-400">Click Rate</p>
<p className="font-bold text-gray-900 dark:text-white">{campaign.clickRate}%</p>
</div>
<div>
<p className="text-gray-600 dark:text-gray-400">Sent</p>
<p className="font-bold text-gray-900 dark:text-white">{campaign.totalSent}</p>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<span className="text-4xl mb-4 block">📧</span>
<p className="text-gray-600 dark:text-gray-400">No campaigns sent yet</p>
<Link href="/admin/newsletter/campaigns">
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
Create Your First Campaign
</button>
</Link>
</div>
)}
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Link href="/admin/newsletter/templates">
<div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg p-6 text-white cursor-pointer hover:from-blue-600 hover:to-blue-700 transition-all">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Email Templates</h3>
<p className="text-blue-100 mt-1">Create reusable templates</p>
</div>
<span className="text-3xl">📧</span>
</div>
</div>
</Link>
<Link href="/admin/newsletter/campaigns">
<div className="bg-gradient-to-r from-green-500 to-green-600 rounded-lg p-6 text-white cursor-pointer hover:from-green-600 hover:to-green-700 transition-all">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Campaigns</h3>
<p className="text-green-100 mt-1">Send targeted newsletters</p>
</div>
<span className="text-3xl">🚀</span>
</div>
</div>
</Link>
<Link href="/admin/newsletter/segments">
<div className="bg-gradient-to-r from-purple-500 to-purple-600 rounded-lg p-6 text-white cursor-pointer hover:from-purple-600 hover:to-purple-700 transition-all">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Segments</h3>
<p className="text-purple-100 mt-1">Target specific audiences</p>
</div>
<span className="text-3xl">🎯</span>
</div>
</div>
</Link>
</div>
</div>
)}
{/* Subscribers Tab */}
{activeTab === 'subscribers' && (
<div className="space-y-6">
{/* Search and Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<input
type="text"
placeholder="Search by email..."
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value, page: 1 })}
className="border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
<select
value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value, page: 1 })}
className="border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<select
value={filters.language}
onChange={(e) => setFilters({ ...filters, language: e.target.value, page: 1 })}
className="border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Languages</option>
<option value="en">English</option>
<option value="fr">French</option>
</select>
<select
value={filters.source}
onChange={(e) => setFilters({ ...filters, source: e.target.value, page: 1 })}
className="border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="all">All Sources</option>
<option value="website">Website</option>
<option value="popup">Popup</option>
<option value="manual">Manual</option>
</select>
</div>
</div>
{/* Subscribers Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Subscriber
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Language
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Source
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Subscribed
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{loading ? (
[...Array(5)].map((_, i) => (
<tr key={i} className="animate-pulse">
<td className="px-6 py-4"><div className="h-4 bg-gray-300 dark:bg-gray-600 rounded"></div></td>
<td className="px-6 py-4"><div className="h-4 bg-gray-300 dark:bg-gray-600 rounded"></div></td>
<td className="px-6 py-4"><div className="h-4 bg-gray-300 dark:bg-gray-600 rounded"></div></td>
<td className="px-6 py-4"><div className="h-4 bg-gray-300 dark:bg-gray-600 rounded"></div></td>
<td className="px-6 py-4"><div className="h-4 bg-gray-300 dark:bg-gray-600 rounded"></div></td>
<td className="px-6 py-4"><div className="h-4 bg-gray-300 dark:bg-gray-600 rounded"></div></td>
</tr>
))
) : subscriptions.length > 0 ? (
subscriptions.map((subscription) => (
<tr key={subscription.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{subscription.email}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs rounded-full ${
subscription.language === 'en' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'
}`}>
{subscription.language.toUpperCase()}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{subscription.source}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs rounded-full ${
subscription.isActive
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
}`}>
{subscription.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{new Date(subscription.subscriptionDate).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<div className="flex space-x-2">
<button className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
Edit
</button>
<button className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
Delete
</button>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="px-6 py-8 text-center">
<div className="text-gray-500 dark:text-gray-400">
<span className="text-4xl mb-4 block">📧</span>
<p>No subscribers found</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Coming Soon for other tabs */}
{['campaigns', 'analytics', 'segments'].includes(activeTab) && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-12 text-center">
<span className="text-6xl mb-6 block">🚧</span>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Coming Soon!
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-6">
This advanced feature is being built with enterprise-level functionality.
</p>
<div className="flex justify-center space-x-4">
<Link href="/admin/newsletter/templates">
<button className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
📧 Manage Templates
</button>
</Link>
<button
onClick={() => setActiveTab('overview')}
className="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg"
>
📊 Back to Overview
</button>
</div>
</div>
)}
</div>
</LayoutWithSidebar>
);
};
export default AdminNewsletter;