carepath-api / scribe /frontend /src /LeadForm.tsx
tranth3truong's picture
Deploy CP-UX-12/13 landing: hero draft proof, privacy and Vietnamese-capability evidence
da09932
Raw
History Blame Contribute Delete
7.78 kB
import { FormEvent, useEffect, useState } from "react";
import { copyFor } from "./content/strings";
import type { Language, Scenario } from "./demo/types";
import {
buildLeadDraft,
defaultLeadMessage,
leadContact,
submitLead,
type LeadSubmissionConfig,
type ProductInterest,
} from "./leads";
interface LeadFormProps {
language: Language;
clinic: string;
specialty: string;
scenario: Scenario;
transcript: string;
endpoint?: string;
leadEmail?: string;
fetcher?: typeof fetch;
onMailto?: (url: string) => void;
interest?: ProductInterest;
onInterestChange?: (interest: ProductInterest) => void;
hideInterestField?: boolean;
}
export default function LeadForm({
language,
clinic,
specialty,
scenario,
transcript,
endpoint = import.meta.env.VITE_LEAD_ENDPOINT,
leadEmail = leadContact.email,
fetcher,
onMailto,
interest: controlledInterest,
onInterestChange,
hideInterestField = false,
}: LeadFormProps) {
const labels = copyFor(language).form;
const [localInterest, setLocalInterest] = useState<ProductInterest>("both");
const interest = controlledInterest ?? localInterest;
const [name, setName] = useState("");
const [clinicName, setClinicName] = useState(clinic);
const [specialtyName, setSpecialtyName] = useState(specialty);
const [role, setRole] = useState("");
const [contact, setContact] = useState("");
const [includeDemoContext, setIncludeDemoContext] = useState(false);
const [message, setMessage] = useState(() =>
defaultLeadMessage(clinic, specialty, scenario, language, interest),
);
const [messageDirty, setMessageDirty] = useState(false);
const [status, setStatus] = useState<
"idle" | "submitting" | "posted" | "mailto" | "error"
>("idle");
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
if (!messageDirty) {
setMessage(
defaultLeadMessage(
clinicName,
specialtyName,
scenario,
language,
interest,
includeDemoContext,
),
);
}
}, [clinicName, includeDemoContext, interest, language, messageDirty, scenario, specialtyName]);
async function sendRequest(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const nextErrors: Record<string, string> = {};
for (const [key, value] of Object.entries({
name,
clinic: clinicName,
specialty: specialtyName,
role,
contact,
message,
})) {
if (!value.trim()) nextErrors[key] = labels.required;
}
setErrors(nextErrors);
if (Object.keys(nextErrors).length > 0) return;
const payload = buildLeadDraft({
clinic: clinicName,
specialty: specialtyName,
scenario,
transcript,
language,
interest,
includeInterpreterContext: includeDemoContext,
fields: { name, role, contact, message },
});
setStatus("submitting");
try {
const submissionConfig: LeadSubmissionConfig = {
endpoint,
email: leadEmail,
};
if (fetcher) submissionConfig.fetcher = fetcher;
if (onMailto) submissionConfig.openMailto = onMailto;
const result = await submitLead(payload, submissionConfig);
setStatus(result);
} catch {
setStatus("error");
}
}
return (
<form
className="lead-form"
data-transcript-length={transcript.length}
onSubmit={sendRequest}
noValidate
>
<label>
{labels.name}
<input
value={name}
maxLength={120}
aria-invalid={Boolean(errors.name)}
aria-describedby={errors.name ? "lead-name-error" : undefined}
onChange={(event) => setName(event.target.value)}
/>
{errors.name && <span className="field-error" id="lead-name-error">{errors.name}</span>}
</label>
<label>
{labels.clinic}
<input
value={clinicName}
maxLength={160}
aria-invalid={Boolean(errors.clinic)}
aria-describedby={errors.clinic ? "lead-clinic-error" : undefined}
onChange={(event) => setClinicName(event.target.value)}
/>
{errors.clinic && <span className="field-error" id="lead-clinic-error">{errors.clinic}</span>}
</label>
<label>
{labels.specialty}
<input
value={specialtyName}
maxLength={100}
aria-invalid={Boolean(errors.specialty)}
aria-describedby={errors.specialty ? "lead-specialty-error" : undefined}
onChange={(event) => setSpecialtyName(event.target.value)}
/>
{errors.specialty && <span className="field-error" id="lead-specialty-error">{errors.specialty}</span>}
</label>
{interest !== "scribe" && (
<label className="lead-form__context">
<input
checked={includeDemoContext}
type="checkbox"
onChange={(event) => setIncludeDemoContext(event.target.checked)}
/>
{labels.includeDemoContext}
</label>
)}
{!hideInterestField && (
<label>
{labels.interest}
<select
value={interest}
onChange={(event) => {
const nextInterest = event.target.value as ProductInterest;
if (controlledInterest === undefined) {
setLocalInterest(nextInterest);
}
onInterestChange?.(nextInterest);
}}
>
<option value="interpreter">
{labels.interestOptions.interpreter}
</option>
<option value="scribe">{labels.interestOptions.scribe}</option>
<option value="both">{labels.interestOptions.both}</option>
</select>
</label>
)}
<label>
{labels.role}
<input
value={role}
maxLength={100}
aria-invalid={Boolean(errors.role)}
aria-describedby={errors.role ? "lead-role-error" : undefined}
onChange={(event) => setRole(event.target.value)}
/>
{errors.role && <span className="field-error" id="lead-role-error">{errors.role}</span>}
</label>
<label>
{labels.contact}
<input
value={contact}
maxLength={160}
aria-invalid={Boolean(errors.contact)}
aria-describedby={errors.contact ? "lead-contact-error" : undefined}
onChange={(event) => setContact(event.target.value)}
/>
{errors.contact && <span className="field-error" id="lead-contact-error">{errors.contact}</span>}
</label>
<label className="lead-form__message">
{labels.message}
<textarea
value={message}
maxLength={1200}
aria-invalid={Boolean(errors.message)}
aria-describedby={errors.message ? "lead-message-error" : undefined}
onChange={(event) => {
setMessage(event.target.value);
setMessageDirty(true);
}}
/>
{errors.message && <span className="field-error" id="lead-message-error">{errors.message}</span>}
</label>
<button
className="button button--primary"
disabled={status === "submitting"}
type="submit"
>
{status === "submitting"
? labels.submitting
: endpoint
? labels.submit
: labels.openMail}
</button>
<p className={status === "error" ? "form-status is-error" : "form-status"} aria-live="polite">
{status === "posted"
? labels.posted
: status === "mailto"
? labels.mailOpened
: status === "error"
? labels.failed
: ""}
</p>
</form>
);
}