![]() 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/newsletter/ |
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';
interface EmailTemplate {
id: string;
name: string;
subject: string;
htmlContent: string;
textContent?: string;
category: string;
thumbnail?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
creator: {
name: string;
email: string;
};
_count: {
campaigns: number;
};
}
const EmailTemplatesPage: React.FC = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
const [previewTemplate, setPreviewTemplate] = useState<EmailTemplate | null>(null);
const [filters, setFilters] = useState({
category: 'all',
page: 1,
limit: 12
});
// Form state
const [formData, setFormData] = useState({
name: '',
subject: '',
htmlContent: '',
textContent: '',
category: 'general'
});
useEffect(() => {
if (status === 'authenticated' && session?.user?.role === 'ADMIN') {
fetchTemplates();
}
}, [session, status, filters]);
const fetchTemplates = async () => {
try {
setLoading(true);
const params = new URLSearchParams({
page: filters.page.toString(),
limit: filters.limit.toString(),
category: filters.category
});
const response = await fetch(`/api/admin/newsletter/templates?${params}`);
const data = await response.json();
if (response.ok) {
setTemplates(data.templates);
} else {
toast.error(data.message || 'Failed to fetch templates');
}
} catch (error) {
toast.error('Error fetching templates');
} finally {
setLoading(false);
}
};
const handleCreateTemplate = async () => {
try {
const response = await fetch('/api/admin/newsletter/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await response.json();
if (response.ok) {
toast.success('Template created successfully!');
setShowCreateModal(false);
setFormData({ name: '', subject: '', htmlContent: '', textContent: '', category: 'general' });
fetchTemplates();
} else {
toast.error(data.message || 'Failed to create template');
}
} catch (error) {
toast.error('Error creating template');
}
};
const handleUpdateTemplate = async () => {
if (!editingTemplate) return;
try {
const response = await fetch('/api/admin/newsletter/templates', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: editingTemplate.id,
...formData
})
});
const data = await response.json();
if (response.ok) {
toast.success('Template updated successfully!');
setEditingTemplate(null);
setFormData({ name: '', subject: '', htmlContent: '', textContent: '', category: 'general' });
fetchTemplates();
} else {
toast.error(data.message || 'Failed to update template');
}
} catch (error) {
toast.error('Error updating template');
}
};
const handleDeleteTemplate = async (template: EmailTemplate) => {
if (template._count.campaigns > 0) {
toast.error(`Cannot delete template. It's being used by ${template._count.campaigns} campaign(s).`);
return;
}
if (!confirm('Are you sure you want to delete this template?')) return;
try {
const response = await fetch('/api/admin/newsletter/templates', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: template.id })
});
const data = await response.json();
if (response.ok) {
toast.success('Template deleted successfully!');
fetchTemplates();
} else {
toast.error(data.message || 'Failed to delete template');
}
} catch (error) {
toast.error('Error deleting template');
}
};
const openEditModal = (template: EmailTemplate) => {
setEditingTemplate(template);
setFormData({
name: template.name,
subject: template.subject,
htmlContent: template.htmlContent,
textContent: template.textContent || '',
category: template.category
});
};
const resetForm = () => {
setFormData({ name: '', subject: '', htmlContent: '', textContent: '', category: 'general' });
setEditingTemplate(null);
setShowCreateModal(false);
};
if (status === 'loading') {
return <div className="flex justify-center items-center min-h-screen">Loading...</div>;
}
if (!session || session.user.role !== 'ADMIN') {
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">
Email Templates
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
Create and manage reusable email templates for newsletters
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2"
>
<span>+</span>
<span>New Template</span>
</button>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-6">
<div className="flex items-center space-x-4">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Category:
</label>
<select
value={filters.category}
onChange={(e) => setFilters({ ...filters, category: 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 Categories</option>
<option value="general">General</option>
<option value="newsletter">Newsletter</option>
<option value="announcement">Announcement</option>
<option value="promotional">Promotional</option>
</select>
</div>
</div>
{/* Templates Grid */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 animate-pulse">
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded mb-4"></div>
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded mb-2"></div>
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded mb-4"></div>
<div className="h-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
</div>
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{templates.map((template) => (
<motion.div
key={template.id}
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 overflow-hidden hover:shadow-md transition-shadow"
>
<div className="p-6">
<div className="flex justify-between items-start mb-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white truncate">
{template.name}
</h3>
<span className={`px-2 py-1 text-xs rounded-full ${
template.category === 'newsletter' ? 'bg-blue-100 text-blue-800' :
template.category === 'announcement' ? 'bg-green-100 text-green-800' :
template.category === 'promotional' ? 'bg-purple-100 text-purple-800' :
'bg-gray-100 text-gray-800'
}`}>
{template.category}
</span>
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-2">
{template.subject}
</p>
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-4">
<span>By {template.creator.name}</span>
<span>{template._count.campaigns} campaigns</span>
</div>
<div className="flex space-x-2">
<button
onClick={() => setPreviewTemplate(template)}
className="flex-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-3 py-2 rounded text-sm hover:bg-gray-200 dark:hover:bg-gray-600"
>
Preview
</button>
<button
onClick={() => openEditModal(template)}
className="flex-1 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 px-3 py-2 rounded text-sm hover:bg-blue-200 dark:hover:bg-blue-800"
>
Edit
</button>
<button
onClick={() => handleDeleteTemplate(template)}
className="bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 px-3 py-2 rounded text-sm hover:bg-red-200 dark:hover:bg-red-800"
disabled={template._count.campaigns > 0}
>
Delete
</button>
</div>
</div>
</motion.div>
))}
</div>
)}
{/* Create/Edit Modal */}
<AnimatePresence>
{(showCreateModal || editingTemplate) && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
>
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{editingTemplate ? 'Edit Template' : 'Create New Template'}
</h2>
</div>
<div className="p-6 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 dark:text-gray-300 mb-2">
Template Name
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full 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"
placeholder="Enter template name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Category
</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
className="w-full 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="general">General</option>
<option value="newsletter">Newsletter</option>
<option value="announcement">Announcement</option>
<option value="promotional">Promotional</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Subject Line
</label>
<input
type="text"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
className="w-full 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"
placeholder="Enter email subject"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
HTML Content
</label>
<textarea
value={formData.htmlContent}
onChange={(e) => setFormData({ ...formData, htmlContent: e.target.value })}
rows={12}
className="w-full 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 font-mono text-sm"
placeholder="Enter HTML content for the email template"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Plain Text Version (Optional)
</label>
<textarea
value={formData.textContent}
onChange={(e) => setFormData({ ...formData, textContent: e.target.value })}
rows={6}
className="w-full 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"
placeholder="Enter plain text version"
/>
</div>
</div>
<div className="p-6 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
<button
onClick={resetForm}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"
>
Cancel
</button>
<button
onClick={editingTemplate ? handleUpdateTemplate : handleCreateTemplate}
disabled={!formData.name || !formData.subject || !formData.htmlContent}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{editingTemplate ? 'Update Template' : 'Create Template'}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* Preview Modal */}
<AnimatePresence>
{previewTemplate && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
>
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Preview: {previewTemplate.name}
</h2>
<button
onClick={() => setPreviewTemplate(null)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
✕
</button>
</div>
<div className="p-6">
<div className="mb-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Subject:</p>
<p className="font-medium text-gray-900 dark:text-white">{previewTemplate.subject}</p>
</div>
<div className="border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden">
<iframe
srcDoc={previewTemplate.htmlContent}
className="w-full h-96"
title="Email Preview"
/>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</LayoutWithSidebar>
);
};
export default EmailTemplatesPage;