95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
"use client"
|
|
|
|
// Adds a contact point (an e-mail address, a phone number, a web address) to a party. The service
|
|
// checks the value against its kind and answers a malformed one in its own words.
|
|
|
|
import type { ProjectSlugs } from "@conusai-kit/ui"
|
|
import { describeError, useWrite } from "@conusai-kit/ui/data"
|
|
import {
|
|
CheckField,
|
|
FormLayout,
|
|
type FormValues,
|
|
PageSection,
|
|
SelectField,
|
|
serviceNotice,
|
|
TextField,
|
|
usePageNavigation,
|
|
} from "@conusai-kit/ui/patterns"
|
|
import { FieldGroup, FieldLegend, FieldSet } from "@conusai-kit/ui/primitives"
|
|
import { type ReactElement, useState } from "react"
|
|
import { CONTACT_KINDS, CONTACT_PURPOSES } from "../contract/constants.js"
|
|
|
|
export interface ContactFormLabels {
|
|
readonly heading: string
|
|
readonly kind: string
|
|
readonly value: string
|
|
readonly purposes: string
|
|
readonly primary: string
|
|
readonly submit: string
|
|
readonly busy: string
|
|
readonly kinds: Readonly<Record<(typeof CONTACT_KINDS)[number], string>>
|
|
readonly purposeNames: Readonly<Record<(typeof CONTACT_PURPOSES)[number], string>>
|
|
/** The text of each business refusal by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`). */
|
|
readonly refusals: Readonly<Record<string, string>>
|
|
}
|
|
|
|
const INPUT_TYPE = { email: "email", phone: "tel", web: "url" } as const
|
|
|
|
export function ContactForm({
|
|
slugs,
|
|
partyId,
|
|
labels,
|
|
}: {
|
|
slugs: ProjectSlugs
|
|
partyId: string
|
|
labels: ContactFormLabels
|
|
}): ReactElement {
|
|
const navigation = usePageNavigation()
|
|
const add = useWrite<Record<string, unknown>, unknown>("crm.contact.add")
|
|
const [requestId, setRequestId] = useState(() => crypto.randomUUID())
|
|
const [kind, setKind] = useState<(typeof CONTACT_KINDS)[number]>("email")
|
|
|
|
const submit = async (values: FormValues): Promise<void> => {
|
|
await add.mutate({
|
|
...slugs,
|
|
partyId,
|
|
kind,
|
|
value: values.value,
|
|
purposes: CONTACT_PURPOSES.filter((purpose) => values[`purpose-${purpose}`] === "on"),
|
|
isPrimary: values.isPrimary === "on",
|
|
requestId,
|
|
})
|
|
setRequestId(crypto.randomUUID())
|
|
// The page is rendered on the server: a reload lists the new contact point.
|
|
await navigation.refresh()
|
|
}
|
|
|
|
return (
|
|
<PageSection title={labels.heading} variant="card">
|
|
<FormLayout
|
|
submitLabel={labels.submit}
|
|
busyLabel={labels.busy}
|
|
onSubmit={submit}
|
|
describeError={(error) => serviceNotice(error, labels.refusals)?.text ?? describeError(error)}
|
|
>
|
|
<SelectField
|
|
label={labels.kind}
|
|
value={kind}
|
|
onValueChange={(value) => setKind(CONTACT_KINDS.find((option) => option === value) ?? "email")}
|
|
options={CONTACT_KINDS.map((option) => ({ value: option, label: labels.kinds[option] }))}
|
|
/>
|
|
<TextField name="value" label={labels.value} type={INPUT_TYPE[kind]} required maxLength={256} />
|
|
<FieldSet>
|
|
<FieldLegend variant="label">{labels.purposes}</FieldLegend>
|
|
<FieldGroup>
|
|
{CONTACT_PURPOSES.map((purpose) => (
|
|
<CheckField key={purpose} name={`purpose-${purpose}`} label={labels.purposeNames[purpose]} />
|
|
))}
|
|
</FieldGroup>
|
|
</FieldSet>
|
|
<CheckField name="isPrimary" label={labels.primary} />
|
|
</FormLayout>
|
|
</PageSection>
|
|
)
|
|
}
|