EMAILOUT / frontend /src /components /workspace /AddContactForm.jsx
Seth
Claude Sonnet 5
Add "Add a contact" form to Leads and Deals detail panels
1d7a01a
Raw History Blame Contribute Delete
4.65 kB
import React, { useState } from 'react';
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { apiFetch } from '@/lib/api';
const EMPTY_DRAFT = { first_name: '', last_name: '', email: '', title: '', phone: '' };
/**
* "Add a contact" mini-form for the associated-contacts section of a company-scoped detail
* panel (Companies / Leads / Deals). Creates the contact via the generic POST /api/contacts
* route with `company` locked to `companyName` — unlike the Companies page's own
* /api/companies/{id}/contacts route, this doesn't require an existing Company row, so it
* works even when the lead/deal's company hasn't synced into one yet.
*/
export default function AddContactForm({ companyName, onAdded, className }) {
const [draft, setDraft] = useState(EMPTY_DRAFT);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const setField = (key, value) => setDraft((d) => ({ ...d, [key]: value }));
const save = async () => {
const email = draft.email.trim();
if (!email) {
setError('Email is required');
return;
}
if (!email.includes('@')) {
setError('Enter a valid email address');
return;
}
setSaving(true);
setError('');
try {
const res = await apiFetch('/api/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first_name: draft.first_name.trim(),
last_name: draft.last_name.trim(),
email,
title: draft.title.trim(),
phone: draft.phone.trim(),
company: companyName,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setError(typeof data.detail === 'string' ? data.detail : 'Could not save contact');
return;
}
setDraft(EMPTY_DRAFT);
onAdded && onAdded(data);
} catch (e) {
console.error(e);
setError('Network error');
} finally {
setSaving(false);
}
};
return (
<div className={className || 'rounded-xl border border-slate-200 bg-slate-50/50 p-4'}>
<h5 className="mb-3 text-sm font-semibold text-slate-700">Add a contact</h5>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<Input
className="bg-white text-sm"
placeholder="First name"
value={draft.first_name}
onChange={(e) => setField('first_name', e.target.value)}
disabled={saving}
/>
<Input
className="bg-white text-sm"
placeholder="Last name"
value={draft.last_name}
onChange={(e) => setField('last_name', e.target.value)}
disabled={saving}
/>
<Input
className="bg-white text-sm sm:col-span-2"
type="email"
placeholder="Email"
value={draft.email}
onChange={(e) => setField('email', e.target.value)}
disabled={saving}
/>
<Input
className="bg-white text-sm"
placeholder="Title"
value={draft.title}
onChange={(e) => setField('title', e.target.value)}
disabled={saving}
/>
<Input
className="bg-white text-sm"
type="tel"
placeholder="Contact number"
value={draft.phone}
onChange={(e) => setField('phone', e.target.value)}
disabled={saving}
/>
</div>
{error ? <p className="mt-2 text-xs text-red-600">{error}</p> : null}
<Button type="button" className="mt-3 w-full" onClick={save} disabled={saving}>
{saving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
Saving…
</>
) : (
'Add contact'
)}
</Button>
</div>
);
}