![]() 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 } from 'framer-motion';
import toast from 'react-hot-toast';
interface NotificationCampaign {
id: string;
type: 'banner' | 'toast' | 'modal' | 'exit-intent';
title: string;
message: string;
actionText?: string;
actionUrl?: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
placement: 'top' | 'bottom' | 'center';
targetPages?: string[];
targetRegion?: string;
language: 'en' | 'fr' | 'both';
isActive: boolean;
expiresAt?: string;
showAfterSeconds?: number;
maxViews?: number;
createdAt: string;
}
const AdminNotifications: React.FC = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [campaigns, setCampaigns] = useState<NotificationCampaign[]>([]);
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingCampaign, setEditingCampaign] = useState<NotificationCampaign | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Form state
const [formData, setFormData] = useState<{
type: 'banner' | 'toast' | 'modal' | 'exit-intent';
title: string;
message: string;
actionText: string;
actionUrl: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
placement: 'top' | 'bottom' | 'center';
targetPages: string;
targetRegion: string;
language: 'en' | 'fr' | 'both';
isActive: boolean;
expiresAt: string;
showAfterSeconds: number;
maxViews: number;
}>({
type: 'banner',
title: '',
message: '',
actionText: '',
actionUrl: '',
priority: 'medium',
placement: 'top',
targetPages: '',
targetRegion: '',
language: 'both',
isActive: true,
expiresAt: '',
showAfterSeconds: 0,
maxViews: 5
});
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/login');
return;
}
if (session?.user?.role !== 'ADMIN') {
router.push('/');
return;
}
fetchCampaigns();
}, [session, status, router]);
const fetchCampaigns = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/public/notifications/campaigns');
if (response.ok) {
const data = await response.json();
setCampaigns(data);
}
} catch (error) {
console.error('Error fetching campaigns:', error);
toast.error('Failed to load notification campaigns');
} finally {
setIsLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.title || !formData.message) {
toast.error('Title and message are required');
return;
}
try {
const campaignData = {
...formData,
id: editingCampaign?.id || `campaign_${Date.now()}`,
targetPages: formData.targetPages ? formData.targetPages.split(',').map(p => p.trim()) : undefined,
createdAt: editingCampaign?.createdAt || new Date().toISOString()
};
if (editingCampaign) {
setCampaigns(prev => prev.map(c => c.id === editingCampaign.id ? campaignData : c));
toast.success('Campaign updated successfully');
} else {
setCampaigns(prev => [...prev, campaignData]);
toast.success('Campaign created successfully');
}
resetForm();
} catch (error) {
toast.error('Failed to save campaign');
}
};
const resetForm = () => {
setFormData({
type: 'banner',
title: '',
message: '',
actionText: '',
actionUrl: '',
priority: 'medium',
placement: 'top',
targetPages: '',
targetRegion: '',
language: 'both',
isActive: true,
expiresAt: '',
showAfterSeconds: 0,
maxViews: 5
});
setEditingCampaign(null);
setShowCreateForm(false);
};
const handleEdit = (campaign: NotificationCampaign) => {
setEditingCampaign(campaign);
setFormData({
type: campaign.type,
title: campaign.title,
message: campaign.message,
actionText: campaign.actionText || '',
actionUrl: campaign.actionUrl || '',
priority: campaign.priority,
placement: campaign.placement,
targetPages: campaign.targetPages?.join(', ') || '',
targetRegion: campaign.targetRegion || '',
language: campaign.language,
isActive: campaign.isActive,
expiresAt: campaign.expiresAt ? campaign.expiresAt.slice(0, 16) : '',
showAfterSeconds: campaign.showAfterSeconds || 0,
maxViews: campaign.maxViews || 5
});
setShowCreateForm(true);
};
const handleToggleActive = (campaignId: string) => {
setCampaigns(prev => prev.map(c =>
c.id === campaignId ? { ...c, isActive: !c.isActive } : c
));
toast.success('Campaign status updated');
};
const handleDelete = (campaignId: string) => {
if (confirm('Are you sure you want to delete this campaign?')) {
setCampaigns(prev => prev.filter(c => c.id !== campaignId));
toast.success('Campaign deleted');
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'urgent': return 'text-red-600 bg-red-100';
case 'high': return 'text-orange-600 bg-orange-100';
case 'medium': return 'text-blue-600 bg-blue-100';
default: return 'text-gray-600 bg-gray-100';
}
};
if (status === 'loading') {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading...</span>
</div>
);
}
return (
<LayoutWithSidebar>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Public Notifications</h1>
<p className="text-gray-600">Manage banners, toasts, and engagement prompts for public users</p>
</div>
<button
onClick={() => setShowCreateForm(true)}
className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition-colors"
>
Create Campaign
</button>
</div>
{/* Campaign Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-lg shadow">
<div className="text-2xl font-bold text-primary">{campaigns.length}</div>
<div className="text-sm text-gray-600">Total Campaigns</div>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<div className="text-2xl font-bold text-green-600">{campaigns.filter(c => c.isActive).length}</div>
<div className="text-sm text-gray-600">Active</div>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<div className="text-2xl font-bold text-red-600">{campaigns.filter(c => c.priority === 'urgent').length}</div>
<div className="text-sm text-gray-600">Urgent</div>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<div className="text-2xl font-bold text-blue-600">{campaigns.filter(c => c.type === 'banner').length}</div>
<div className="text-sm text-gray-600">Banners</div>
</div>
</div>
{/* Create/Edit Form */}
{showCreateForm && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-lg shadow-lg p-6"
>
<h2 className="text-xl font-bold mb-4">
{editingCampaign ? 'Edit Campaign' : 'Create New Campaign'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Campaign Type
</label>
<select
value={formData.type}
onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value as any }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
>
<option value="banner">Banner</option>
<option value="toast">Toast</option>
<option value="modal">Modal</option>
<option value="exit-intent">Exit Intent</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Priority
</label>
<select
value={formData.priority}
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as any }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Title
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Message
</label>
<textarea
value={formData.message}
onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2 h-24"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Action Text (Optional)
</label>
<input
type="text"
value={formData.actionText}
onChange={(e) => setFormData(prev => ({ ...prev, actionText: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Action URL (Optional)
</label>
<input
type="text"
value={formData.actionUrl}
onChange={(e) => setFormData(prev => ({ ...prev, actionUrl: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Language
</label>
<select
value={formData.language}
onChange={(e) => setFormData(prev => ({ ...prev, language: e.target.value as any }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
>
<option value="both">Both Languages</option>
<option value="en">English Only</option>
<option value="fr">French Only</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Max Views
</label>
<input
type="number"
value={formData.maxViews}
onChange={(e) => setFormData(prev => ({ ...prev, maxViews: parseInt(e.target.value) }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
min="1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Expires At (Optional)
</label>
<input
type="datetime-local"
value={formData.expiresAt}
onChange={(e) => setFormData(prev => ({ ...prev, expiresAt: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Target Pages (comma-separated, optional)
</label>
<input
type="text"
value={formData.targetPages}
onChange={(e) => setFormData(prev => ({ ...prev, targetPages: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="/, /class-action, /faq"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isActive"
checked={formData.isActive}
onChange={(e) => setFormData(prev => ({ ...prev, isActive: e.target.checked }))}
className="mr-2"
/>
<label htmlFor="isActive" className="text-sm font-medium text-gray-700">
Active
</label>
</div>
<div className="flex space-x-4">
<button
type="submit"
className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition-colors"
>
{editingCampaign ? 'Update Campaign' : 'Create Campaign'}
</button>
<button
type="button"
onClick={resetForm}
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400 transition-colors"
>
Cancel
</button>
</div>
</form>
</motion.div>
)}
{/* Campaigns List */}
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Active Campaigns</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Campaign
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Priority
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Expires
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{campaigns.map((campaign) => (
<tr key={campaign.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">{campaign.title}</div>
<div className="text-sm text-gray-500">{campaign.message.slice(0, 60)}...</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{campaign.type}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPriorityColor(campaign.priority)}`}>
{campaign.priority}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
campaign.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}>
{campaign.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{campaign.expiresAt
? new Date(campaign.expiresAt).toLocaleDateString()
: 'Never'
}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button
onClick={() => handleEdit(campaign)}
className="text-primary hover:text-primary-dark"
>
Edit
</button>
<button
onClick={() => handleToggleActive(campaign.id)}
className={campaign.isActive ? 'text-red-600 hover:text-red-900' : 'text-green-600 hover:text-green-900'}
>
{campaign.isActive ? 'Deactivate' : 'Activate'}
</button>
<button
onClick={() => handleDelete(campaign.id)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{campaigns.length === 0 && (
<div className="text-center py-12">
<div className="text-gray-500">No notification campaigns found</div>
<button
onClick={() => setShowCreateForm(true)}
className="mt-4 bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition-colors"
>
Create Your First Campaign
</button>
</div>
)}
</div>
</div>
</LayoutWithSidebar>
);
};
export default AdminNotifications;