237 lines
10 KiB
TypeScript
237 lines
10 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useEmailsStore } from '@/stores/emails';
|
|
import { useClientsStore } from '@/stores/clients';
|
|
import { Mail, Send, Trash2, Edit3, Sparkles, Gift } from 'lucide-react';
|
|
import { cn, formatDate } from '@/lib/utils';
|
|
import { EmailStatusBadge } from '@/components/Badge';
|
|
import EmptyState from '@/components/EmptyState';
|
|
import { PageLoader } from '@/components/LoadingSpinner';
|
|
import LoadingSpinner from '@/components/LoadingSpinner';
|
|
import Modal from '@/components/Modal';
|
|
|
|
export default function EmailsPage() {
|
|
const { emails, isLoading, isGenerating, statusFilter, setStatusFilter, fetchEmails, generateEmail, generateBirthdayEmail, updateEmail, sendEmail, deleteEmail } = useEmailsStore();
|
|
const { clients, fetchClients } = useClientsStore();
|
|
const [showCompose, setShowCompose] = useState(false);
|
|
const [editingEmail, setEditingEmail] = useState<string | null>(null);
|
|
const [editSubject, setEditSubject] = useState('');
|
|
const [editContent, setEditContent] = useState('');
|
|
const [composeForm, setComposeForm] = useState({ clientId: '', purpose: '', provider: 'anthropic' as 'anthropic' | 'openai' });
|
|
|
|
useEffect(() => {
|
|
fetchEmails();
|
|
fetchClients();
|
|
}, [fetchEmails, fetchClients]);
|
|
|
|
const filtered = statusFilter ? emails.filter((e) => e.status === statusFilter) : emails;
|
|
|
|
const handleGenerate = async () => {
|
|
try {
|
|
await generateEmail(composeForm.clientId, composeForm.purpose, composeForm.provider);
|
|
setShowCompose(false);
|
|
setComposeForm({ clientId: '', purpose: '', provider: 'anthropic' });
|
|
} catch {}
|
|
};
|
|
|
|
const handleGenerateBirthday = async () => {
|
|
try {
|
|
await generateBirthdayEmail(composeForm.clientId, composeForm.provider);
|
|
setShowCompose(false);
|
|
setComposeForm({ clientId: '', purpose: '', provider: 'anthropic' });
|
|
} catch {}
|
|
};
|
|
|
|
const startEdit = (email: typeof emails[0]) => {
|
|
setEditingEmail(email.id);
|
|
setEditSubject(email.subject);
|
|
setEditContent(email.content);
|
|
};
|
|
|
|
const saveEdit = async () => {
|
|
if (!editingEmail) return;
|
|
await updateEmail(editingEmail, { subject: editSubject, content: editContent });
|
|
setEditingEmail(null);
|
|
};
|
|
|
|
if (isLoading && emails.length === 0) return <PageLoader />;
|
|
|
|
const statusFilters = [
|
|
{ key: null, label: 'All' },
|
|
{ key: 'draft', label: 'Drafts' },
|
|
{ key: 'sent', label: 'Sent' },
|
|
{ key: 'failed', label: 'Failed' },
|
|
];
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto space-y-5 animate-fade-in">
|
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">Emails</h1>
|
|
<p className="text-slate-500 text-sm mt-1">AI-generated emails for your network</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowCompose(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Sparkles className="w-4 h-4" />
|
|
Compose
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex gap-2">
|
|
{statusFilters.map(({ key, label }) => (
|
|
<button
|
|
key={label}
|
|
onClick={() => setStatusFilter(key)}
|
|
className={cn(
|
|
'px-3 py-1.5 rounded-full text-sm font-medium transition-colors',
|
|
statusFilter === key
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
|
)}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Emails list */}
|
|
{filtered.length === 0 ? (
|
|
<EmptyState
|
|
icon={Mail}
|
|
title="No emails"
|
|
description="Generate AI-powered emails for your clients"
|
|
action={{ label: 'Compose Email', onClick: () => setShowCompose(true) }}
|
|
/>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{filtered.map((email) => (
|
|
<div key={email.id} className="bg-white border border-slate-200 rounded-xl p-5">
|
|
{editingEmail === email.id ? (
|
|
<div className="space-y-3">
|
|
<input
|
|
value={editSubject}
|
|
onChange={(e) => setEditSubject(e.target.value)}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
<textarea
|
|
value={editContent}
|
|
onChange={(e) => setEditContent(e.target.value)}
|
|
rows={8}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
|
|
/>
|
|
<div className="flex gap-2 justify-end">
|
|
<button onClick={() => setEditingEmail(null)} className="px-3 py-1.5 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">Cancel</button>
|
|
<button onClick={saveEdit} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">Save</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<EmailStatusBadge status={email.status} />
|
|
{email.client && (
|
|
<span className="text-xs text-slate-500">
|
|
To: {email.client.firstName} {email.client.lastName}
|
|
</span>
|
|
)}
|
|
<span className="text-xs text-slate-400">{formatDate(email.createdAt)}</span>
|
|
</div>
|
|
<h3 className="font-semibold text-slate-900 mb-2">{email.subject}</h3>
|
|
<p className="text-sm text-slate-600 whitespace-pre-wrap line-clamp-4">{email.content}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 mt-4 pt-3 border-t border-slate-100">
|
|
{email.status === 'draft' && (
|
|
<>
|
|
<button
|
|
onClick={() => startEdit(email)}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
|
|
>
|
|
<Edit3 className="w-3.5 h-3.5" /> Edit
|
|
</button>
|
|
<button
|
|
onClick={async () => { await sendEmail(email.id); }}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-50 text-blue-700 hover:bg-blue-100 rounded-lg transition-colors"
|
|
>
|
|
<Send className="w-3.5 h-3.5" /> Send
|
|
</button>
|
|
</>
|
|
)}
|
|
<button
|
|
onClick={async () => { if (confirm('Delete this email?')) await deleteEmail(email.id); }}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors ml-auto"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" /> Delete
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Compose Modal */}
|
|
<Modal isOpen={showCompose} onClose={() => setShowCompose(false)} title="Generate Email" size="md">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Client *</label>
|
|
<select
|
|
value={composeForm.clientId}
|
|
onChange={(e) => setComposeForm({ ...composeForm, clientId: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value="">Select a client...</option>
|
|
{clients.map((c) => (
|
|
<option key={c.id} value={c.id}>{c.firstName} {c.lastName}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Purpose</label>
|
|
<textarea
|
|
value={composeForm.purpose}
|
|
onChange={(e) => setComposeForm({ ...composeForm, purpose: e.target.value })}
|
|
rows={3}
|
|
placeholder="What's this email about?"
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Provider</label>
|
|
<select
|
|
value={composeForm.provider}
|
|
onChange={(e) => setComposeForm({ ...composeForm, provider: e.target.value as any })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value="anthropic">Anthropic (Claude)</option>
|
|
<option value="openai">OpenAI (GPT)</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={!composeForm.clientId || !composeForm.purpose || isGenerating}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
|
>
|
|
{isGenerating ? <LoadingSpinner size="sm" className="text-white" /> : <Sparkles className="w-4 h-4" />}
|
|
Generate
|
|
</button>
|
|
<button
|
|
onClick={handleGenerateBirthday}
|
|
disabled={!composeForm.clientId || isGenerating}
|
|
className="flex items-center gap-2 px-4 py-2.5 bg-pink-50 text-pink-700 rounded-lg text-sm font-medium hover:bg-pink-100 disabled:opacity-50 transition-colors"
|
|
>
|
|
<Gift className="w-4 h-4" />
|
|
Birthday
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|