EMAILOUT / frontend /src /components /workspace /ContactIdentityEditor.jsx
Seth
update
713b4f3
import React, { useEffect, useState, useRef } from 'react';
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
/** API/CSV may return numbers; coerce before trim/compare so Inputs never throw. */
function strField(v) {
return String(v ?? '').trim();
}
function baselineFromProps(firstName, lastName, email, title) {
return {
firstName: String(firstName ?? ''),
lastName: String(lastName ?? ''),
email: String(email ?? ''),
title: String(title ?? ''),
};
}
function formsEqual(a, b) {
return (
strField(a.firstName) === strField(b.firstName) &&
strField(a.lastName) === strField(b.lastName) &&
strField(a.email) === strField(b.email) &&
strField(a.title) === strField(b.title)
);
}
/**
* Editable person fields (first / last name, email, title).
* `autoSave`: debounced PATCH after typing pauses (no Save button).
* Otherwise: Save button commits all fields.
*/
export default function ContactIdentityEditor({
firstName = '',
lastName = '',
email = '',
title = '',
onSave,
disabled = false,
className,
heading = 'Contact details',
autoSave = false,
}) {
const [form, setForm] = useState(() => baselineFromProps(firstName, lastName, email, title));
const [saving, setSaving] = useState(false);
const baselineRef = useRef(baselineFromProps(firstName, lastName, email, title));
useEffect(() => {
const b = baselineFromProps(firstName, lastName, email, title);
baselineRef.current = b;
setForm(b);
}, [firstName, lastName, email, title]);
const setField = (key, value) => setForm((f) => ({ ...f, [key]: value }));
const handleSave = async () => {
if (!onSave || disabled) return;
setSaving(true);
try {
await onSave({
first_name: form.firstName,
last_name: form.lastName,
email: form.email,
title: form.title,
});
} finally {
setSaving(false);
}
};
useEffect(() => {
if (!autoSave || !onSave || disabled) return;
const baseline = baselineRef.current;
if (formsEqual(form, baseline)) return;
const t = setTimeout(async () => {
setSaving(true);
try {
await onSave({
first_name: form.firstName,
last_name: form.lastName,
email: form.email,
title: form.title,
});
baselineRef.current = {
firstName: form.firstName,
lastName: form.lastName,
email: form.email,
title: form.title,
};
} finally {
setSaving(false);
}
}, 650);
return () => clearTimeout(t);
}, [form, autoSave, disabled, onSave, firstName, lastName, email, title]);
return (
<div className={cn('rounded-xl border border-slate-200 bg-white p-4 mb-6', className)}>
<div className="mb-3 flex items-center justify-between gap-2">
<h4 className="text-sm font-semibold text-slate-800">{heading}</h4>
{autoSave && saving ? (
<span className="flex items-center gap-1 text-xs text-slate-500">
<Loader2 className="h-3 w-3 animate-spin shrink-0" aria-hidden />
Saving…
</span>
) : null}
</div>
<div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
<div>
<label className="mb-1 block text-xs font-medium text-slate-500">First name</label>
<Input
value={form.firstName}
onChange={(e) => setField('firstName', e.target.value)}
disabled={disabled}
className="text-sm"
/>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-slate-500">Last name</label>
<Input
value={form.lastName}
onChange={(e) => setField('lastName', e.target.value)}
disabled={disabled}
className="text-sm"
/>
</div>
<div className="sm:col-span-2">
<label className="mb-1 block text-xs font-medium text-slate-500">Email</label>
<Input
type="email"
value={form.email}
onChange={(e) => setField('email', e.target.value)}
disabled={disabled}
className="text-sm"
/>
</div>
<div className="sm:col-span-2">
<label className="mb-1 block text-xs font-medium text-slate-500">Title</label>
<Input
value={form.title}
onChange={(e) => setField('title', e.target.value)}
disabled={disabled}
className="text-sm"
/>
</div>
</div>
{!autoSave ? (
<div className="mt-4">
<Button
type="button"
className="w-full sm:w-auto"
onClick={handleSave}
disabled={disabled || saving}
>
{saving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
Saving…
</>
) : (
'Save'
)}
</Button>
</div>
) : null}
</div>
);
}