@doany-ai/forms 0.1.0-alpha.0 → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import { Label } from "@doany-ai/app-core/ui/label";
7
7
  import { Skeleton } from "@doany-ai/app-core/ui/skeleton";
8
8
  import { Textarea } from "@doany-ai/app-core/ui/textarea";
9
9
  import { CheckCircle2, Loader2 } from "lucide-react";
10
+ import { isE164, normalizePhone } from "../phone.js";
10
11
  import { useEffect, useMemo, useRef, useState } from "react";
11
12
  import { useForm, useSubmitForm } from "../hooks.js";
12
13
  import { formLabels } from "../labels.js";
@@ -80,7 +81,14 @@ function Question({ field, value, onChange, labels }) {
80
81
  }
81
82
  const inputType = field.type === "email" ? "email" : field.type === "phone" ? "tel" : field.type === "number" ? "number" : field.type === "date" ? "date" : "text";
82
83
  const autoComplete = field.type === "email" ? "email" : field.type === "phone" ? "tel" : undefined;
83
- return (_jsxs("div", { className: "space-y-2", children: [label, help, _jsx(Input, { id: id, type: inputType, autoComplete: autoComplete, value: typeof value === "number" ? String(value) : text, onChange: (e) => onChange(field.type === "number" ? (e.target.value === "" ? "" : Number(e.target.value)) : e.target.value), required: field.required, minLength: field.validation?.min_length, maxLength: field.validation?.max_length, min: field.validation?.min, max: field.validation?.max, className: fieldClass })] }));
84
+ return (_jsxs("div", { className: "space-y-2", children: [label, help, _jsx(Input, { id: id, type: inputType, autoComplete: autoComplete, value: typeof value === "number" ? String(value) : text, onChange: (e) => {
85
+ if (field.type === "phone") {
86
+ // The platform takes E.164 only: say so before the submit does.
87
+ const normalized = normalizePhone(e.target.value);
88
+ e.target.setCustomValidity(!normalized || isE164(normalized) ? "" : labels.phoneFormat);
89
+ }
90
+ onChange(field.type === "number" ? (e.target.value === "" ? "" : Number(e.target.value)) : e.target.value);
91
+ }, required: field.required, minLength: field.validation?.min_length, maxLength: field.validation?.max_length, min: field.validation?.min, max: field.validation?.max, className: fieldClass })] }));
84
92
  }
85
93
  /** The answers the platform takes: `hidden` values as given, numbers as numbers, nothing for blanks. */
86
94
  function answersOf(view, answers) {
@@ -88,14 +96,16 @@ function answersOf(view, answers) {
88
96
  for (const field of view.definition.fields) {
89
97
  if (field.type === "statement")
90
98
  continue;
91
- if (field.type === "consent" && field.consent?.mode === "on_submit") {
92
- out[field.key] = true;
99
+ // The backend writes an on-submit consent itself and refuses a value for it.
100
+ if (field.type === "consent" && field.consent?.mode === "on_submit")
93
101
  continue;
94
- }
95
102
  const value = answers[field.key];
96
103
  if (value === undefined || value === "" || value === null)
97
104
  continue;
98
- out[field.key] = value;
105
+ // Every option cleared again is no answer: the platform refuses an empty list.
106
+ if (Array.isArray(value) && value.length === 0)
107
+ continue;
108
+ out[field.key] = field.type === "phone" && typeof value === "string" ? normalizePhone(value) : value;
99
109
  }
100
110
  return out;
101
111
  }
package/dist/labels.d.ts CHANGED
@@ -32,6 +32,8 @@ export interface FormLabels {
32
32
  choose: string;
33
33
  /** Beside a required question. */
34
34
  required: string;
35
+ /** Shown while a phone number is not in the international format. */
36
+ phoneFormat: string;
35
37
  }
36
38
  export declare const defaultFormLabels: FormLabels;
37
39
  /** The defaults with a site's changes on top. */
package/dist/labels.js CHANGED
@@ -16,6 +16,7 @@ export const defaultFormLabels = {
16
16
  stepOf: (step, of) => `Step ${step} of ${of}`,
17
17
  choose: "Choose…",
18
18
  required: "required",
19
+ phoneFormat: "Use the international format, like +1 415 555 0132.",
19
20
  };
20
21
  /** The defaults with a site's changes on top. */
21
22
  export const formLabels = (labels) => (labels ? { ...defaultFormLabels, ...labels } : defaultFormLabels);
@@ -0,0 +1,4 @@
1
+ /** The platform takes phone numbers in E.164 (`+14155550132`): the visitor's
2
+ * spaces, dots, dashes and brackets are dropped and a `00` prefix becomes `+`. */
3
+ export declare function normalizePhone(raw: string): string;
4
+ export declare const isE164: (value: string) => boolean;
package/dist/phone.js ADDED
@@ -0,0 +1,7 @@
1
+ /** The platform takes phone numbers in E.164 (`+14155550132`): the visitor's
2
+ * spaces, dots, dashes and brackets are dropped and a `00` prefix becomes `+`. */
3
+ export function normalizePhone(raw) {
4
+ const trimmed = raw.trim().replace(/[\s().-]/g, "");
5
+ return trimmed.startsWith("00") ? `+${trimmed.slice(2)}` : trimmed;
6
+ }
7
+ export const isE164 = (value) => /^\+[1-9][0-9]{1,14}$/.test(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doany-ai/forms",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.1",
4
4
  "description": "The business's forms on a Doany site: a form hook and a default component that renders a form's questions and submits the answers",
5
5
  "type": "module",
6
6
  "sideEffects": false,