Files
network-app-web/src/pages/EmailsPage.tsx

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>
);
}