@@ -0,0 +1,94 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user