@squaredr/fieldcraft-pro 1.8.0 → 1.9.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.
- package/dist/{chunk-IW654Z3M.mjs → chunk-5QXKDYWY.mjs} +92 -5
- package/dist/{chunk-MLP5EDWP.mjs → chunk-7GPX3YIB.mjs} +35 -61
- package/dist/{chunk-GXKQCXIB.mjs → chunk-DWRLPGX5.mjs} +395 -86
- package/dist/{chunk-CYXJGOQH.mjs → chunk-KTIU7VCP.mjs} +68 -9
- package/dist/default-schema-BGxf9Jrg.d.ts +174 -0
- package/dist/default-schema-Bwq8FATF.d.mts +174 -0
- package/dist/form-builder/index.d.mts +14 -170
- package/dist/form-builder/index.d.ts +14 -170
- package/dist/form-builder/index.js +396 -85
- package/dist/form-builder/index.mjs +1 -1
- package/dist/index.d.mts +109 -8
- package/dist/index.d.ts +109 -8
- package/dist/index.js +1697 -217
- package/dist/index.mjs +1106 -11
- package/dist/response-viewer/index.js +36 -61
- package/dist/response-viewer/index.mjs +1 -1
- package/dist/styles.css +2 -2
- package/dist/templates/index.d.mts +1 -1
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.js +92 -5
- package/dist/templates/index.mjs +1 -1
- package/dist/theme-editor/index.d.mts +1 -2
- package/dist/theme-editor/index.d.ts +1 -2
- package/dist/theme-editor/index.js +68 -9
- package/dist/theme-editor/index.mjs +1 -1
- package/dist/{types-bFHsZT1n.d.mts → types-BQOeI3LX.d.mts} +1 -1
- package/dist/{types-bFHsZT1n.d.ts → types-BQOeI3LX.d.ts} +1 -1
- package/package.json +20 -20
- package/theme-editor/styles.css +29 -0
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
|
37
37
|
var SelectPrimitive__namespace = /*#__PURE__*/_interopNamespace(SelectPrimitive);
|
|
38
38
|
|
|
39
39
|
// package.json
|
|
40
|
-
var version = "1.
|
|
40
|
+
var version = "1.9.1";
|
|
41
41
|
var PRODUCT_IDS = {
|
|
42
42
|
FIELDCRAFT_PRO: 1
|
|
43
43
|
};
|
|
@@ -592,21 +592,20 @@ var MAX_HISTORY = 50;
|
|
|
592
592
|
function useUndoRedo(currentSchema, setSchema) {
|
|
593
593
|
const historyRef = React.useRef([currentSchema]);
|
|
594
594
|
const [currentIndex, setCurrentIndex] = React.useState(0);
|
|
595
|
-
const currentIndexRef = React.useRef(currentIndex);
|
|
596
|
-
currentIndexRef.current = currentIndex;
|
|
597
595
|
const canUndo = currentIndex > 0;
|
|
598
596
|
const canRedo = currentIndex < historyRef.current.length - 1;
|
|
599
597
|
const push = React.useCallback(
|
|
600
598
|
(schema) => {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
historyRef.current.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
599
|
+
setCurrentIndex((prevIndex) => {
|
|
600
|
+
historyRef.current = historyRef.current.slice(0, prevIndex + 1);
|
|
601
|
+
historyRef.current.push(schema);
|
|
602
|
+
let newIndex = prevIndex + 1;
|
|
603
|
+
if (historyRef.current.length > MAX_HISTORY) {
|
|
604
|
+
historyRef.current.shift();
|
|
605
|
+
newIndex = Math.max(0, newIndex - 1);
|
|
606
|
+
}
|
|
607
|
+
return newIndex;
|
|
608
|
+
});
|
|
610
609
|
},
|
|
611
610
|
[]
|
|
612
611
|
);
|
|
@@ -639,13 +638,9 @@ function useUndoRedo(currentSchema, setSchema) {
|
|
|
639
638
|
}
|
|
640
639
|
|
|
641
640
|
// src/form-builder/utils/id-generator.ts
|
|
642
|
-
var counter = 0;
|
|
643
641
|
function generateId(prefix) {
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
counter = (counter + 1) % 1e4;
|
|
647
|
-
const count = counter.toString(36);
|
|
648
|
-
return `${prefix}_${timestamp}${count}${random}`;
|
|
642
|
+
const uuid = crypto.randomUUID().replace(/-/g, "").substring(0, 12);
|
|
643
|
+
return `${prefix}_${uuid}`;
|
|
649
644
|
}
|
|
650
645
|
function generateSectionId() {
|
|
651
646
|
return generateId("section");
|
|
@@ -732,18 +727,39 @@ function duplicateSection(schema, sectionId) {
|
|
|
732
727
|
const sectionIndex = newSchema.sections.findIndex((s) => s.id === sectionId);
|
|
733
728
|
if (sectionIndex === -1) return schema;
|
|
734
729
|
const original = newSchema.sections[sectionIndex];
|
|
730
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const q of original.questions) {
|
|
732
|
+
idMap.set(q.id, generateQuestionId());
|
|
733
|
+
}
|
|
735
734
|
const duplicate = {
|
|
736
735
|
...original,
|
|
737
736
|
id: generateSectionId(),
|
|
738
737
|
title: `${original.title} (Copy)`,
|
|
738
|
+
showIf: original.showIf ? remapConditionFieldIds(original.showIf, idMap) : void 0,
|
|
739
739
|
questions: original.questions.map((q) => ({
|
|
740
740
|
...q,
|
|
741
|
-
id:
|
|
741
|
+
id: idMap.get(q.id),
|
|
742
|
+
showIf: q.showIf ? remapConditionFieldIds(q.showIf, idMap) : void 0
|
|
742
743
|
}))
|
|
743
744
|
};
|
|
744
745
|
newSchema.sections.splice(sectionIndex + 1, 0, duplicate);
|
|
745
746
|
return newSchema;
|
|
746
747
|
}
|
|
748
|
+
function remapConditionFieldIds(expr, idMap) {
|
|
749
|
+
if (expr.field) {
|
|
750
|
+
return {
|
|
751
|
+
...expr,
|
|
752
|
+
field: idMap.get(expr.field) ?? expr.field
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
if (expr.conditions) {
|
|
756
|
+
return {
|
|
757
|
+
...expr,
|
|
758
|
+
conditions: expr.conditions.map((c) => remapConditionFieldIds(c, idMap))
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
return expr;
|
|
762
|
+
}
|
|
747
763
|
function addQuestion(schema, sectionId, question, index) {
|
|
748
764
|
const newSchema = deepClone(schema);
|
|
749
765
|
const section = newSchema.sections.find((s) => s.id === sectionId);
|
|
@@ -887,7 +903,7 @@ function useBuilderState(initialSchema) {
|
|
|
887
903
|
(sectionId) => {
|
|
888
904
|
applyMutation((s) => removeSection(s, sectionId));
|
|
889
905
|
setSelectedItem((prev) => {
|
|
890
|
-
if (prev?.
|
|
906
|
+
if (prev?.sectionId === sectionId) return null;
|
|
891
907
|
return prev;
|
|
892
908
|
});
|
|
893
909
|
},
|
|
@@ -2020,6 +2036,14 @@ function SectionBlock({ section, builderState }) {
|
|
|
2020
2036
|
}
|
|
2021
2037
|
function FormCanvas({ builderState }) {
|
|
2022
2038
|
const { schema } = builderState;
|
|
2039
|
+
const [localTitle, setLocalTitle] = React.useState(schema.title);
|
|
2040
|
+
const [localDesc, setLocalDesc] = React.useState(schema.description ?? "");
|
|
2041
|
+
React.useEffect(() => {
|
|
2042
|
+
setLocalTitle(schema.title);
|
|
2043
|
+
}, [schema.title]);
|
|
2044
|
+
React.useEffect(() => {
|
|
2045
|
+
setLocalDesc(schema.description ?? "");
|
|
2046
|
+
}, [schema.description]);
|
|
2023
2047
|
const handleAddSection = () => {
|
|
2024
2048
|
const newSection = {
|
|
2025
2049
|
id: generateSectionId(),
|
|
@@ -2036,9 +2060,12 @@ function FormCanvas({ builderState }) {
|
|
|
2036
2060
|
"input",
|
|
2037
2061
|
{
|
|
2038
2062
|
type: "text",
|
|
2039
|
-
value:
|
|
2040
|
-
onChange: (e) =>
|
|
2041
|
-
|
|
2063
|
+
value: localTitle,
|
|
2064
|
+
onChange: (e) => setLocalTitle(e.target.value),
|
|
2065
|
+
onBlur: () => {
|
|
2066
|
+
if (localTitle !== schema.title) {
|
|
2067
|
+
builderState.updateSchema({ ...schema, title: localTitle });
|
|
2068
|
+
}
|
|
2042
2069
|
},
|
|
2043
2070
|
className: "w-full text-xl font-bold bg-transparent text-foreground border-0 outline-none p-1.5 rounded-md focus:bg-card transition-colors",
|
|
2044
2071
|
placeholder: "Form Title"
|
|
@@ -2047,9 +2074,12 @@ function FormCanvas({ builderState }) {
|
|
|
2047
2074
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2048
2075
|
"textarea",
|
|
2049
2076
|
{
|
|
2050
|
-
value:
|
|
2051
|
-
onChange: (e) =>
|
|
2052
|
-
|
|
2077
|
+
value: localDesc,
|
|
2078
|
+
onChange: (e) => setLocalDesc(e.target.value),
|
|
2079
|
+
onBlur: () => {
|
|
2080
|
+
if (localDesc !== (schema.description ?? "")) {
|
|
2081
|
+
builderState.updateSchema({ ...schema, description: localDesc || void 0 });
|
|
2082
|
+
}
|
|
2053
2083
|
},
|
|
2054
2084
|
className: "w-full text-sm text-muted-foreground bg-transparent border-0 outline-none p-1.5 resize-y min-h-12 rounded-md leading-relaxed focus:bg-card transition-colors",
|
|
2055
2085
|
placeholder: "Form description (optional)"
|
|
@@ -2152,12 +2182,26 @@ SelectItem.displayName = "SelectItem";
|
|
|
2152
2182
|
function useConfigUpdater(question, onUpdate) {
|
|
2153
2183
|
return (updates) => {
|
|
2154
2184
|
const current = question.config ?? {};
|
|
2155
|
-
|
|
2185
|
+
const merged = { ...current, type: question.type, ...updates };
|
|
2186
|
+
for (const key of Object.keys(merged)) {
|
|
2187
|
+
if (merged[key] === void 0) delete merged[key];
|
|
2188
|
+
}
|
|
2189
|
+
onUpdate({ config: merged });
|
|
2156
2190
|
};
|
|
2157
2191
|
}
|
|
2158
|
-
function QuestionConfigEditor({ question, onUpdate }) {
|
|
2192
|
+
function QuestionConfigEditor({ question, schema, onUpdate }) {
|
|
2159
2193
|
const updateConfig = useConfigUpdater(question, onUpdate);
|
|
2160
2194
|
const config = question.config ?? {};
|
|
2195
|
+
const allQuestions = React.useMemo(() => {
|
|
2196
|
+
if (!schema?.sections) return [];
|
|
2197
|
+
return schema.sections.flatMap((s) => s.questions || []);
|
|
2198
|
+
}, [schema]);
|
|
2199
|
+
const candidateAmountFields = React.useMemo(() => {
|
|
2200
|
+
return allQuestions.filter((q) => q.id !== question.id && ["calculated", "number", "slider", "rating", "single_select"].includes(q.type)).map((q) => ({ label: `${q.label || q.id} (${q.id})`, value: q.id }));
|
|
2201
|
+
}, [allQuestions, question.id]);
|
|
2202
|
+
const candidateTimezoneFields = React.useMemo(() => {
|
|
2203
|
+
return allQuestions.filter((q) => q.id !== question.id && ["dropdown", "single_select", "short_text"].includes(q.type)).map((q) => ({ label: `${q.label || q.id} (${q.id})`, value: q.id }));
|
|
2204
|
+
}, [allQuestions, question.id]);
|
|
2161
2205
|
switch (question.type) {
|
|
2162
2206
|
// ── Text ──
|
|
2163
2207
|
case "short_text":
|
|
@@ -2181,32 +2225,32 @@ function QuestionConfigEditor({ question, onUpdate }) {
|
|
|
2181
2225
|
// ── Numeric ──
|
|
2182
2226
|
case "number":
|
|
2183
2227
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Number Settings", children: [
|
|
2184
|
-
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min", value: config.min, onChange: (v) => updateConfig({ min: v }) }),
|
|
2185
|
-
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max", value: config.max, onChange: (v) => updateConfig({ max: v }) }),
|
|
2228
|
+
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min Value", value: config.min, onChange: (v) => updateConfig({ min: v }) }),
|
|
2229
|
+
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Value", value: config.max, onChange: (v) => updateConfig({ max: v }) }),
|
|
2186
2230
|
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig({ step: v }) }),
|
|
2187
|
-
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Decimal Places", value: config.decimalPlaces, placeholder: "0", onChange: (v) => updateConfig({ decimalPlaces: v }) }),
|
|
2188
2231
|
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Prefix", value: config.prefix, placeholder: "e.g. $", onChange: (v) => updateConfig({ prefix: v }) }),
|
|
2189
2232
|
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Suffix", value: config.suffix, placeholder: "e.g. kg", onChange: (v) => updateConfig({ suffix: v }) })
|
|
2190
2233
|
] });
|
|
2191
2234
|
case "slider":
|
|
2192
2235
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Slider Settings", children: [
|
|
2193
|
-
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min", value: config.min, placeholder: "0", onChange: (v) => updateConfig({ min: v }) }),
|
|
2194
|
-
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max", value: config.max, placeholder: "100", onChange: (v) => updateConfig({ max: v }) }),
|
|
2195
|
-
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig({ step: v }) }),
|
|
2196
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2197
|
-
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "
|
|
2198
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2236
|
+
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Min Value", value: config.min, placeholder: "0", onChange: (v) => updateConfig({ min: v ?? 0 }) }),
|
|
2237
|
+
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Value", value: config.max, placeholder: "100", onChange: (v) => updateConfig({ max: v ?? 100 }) }),
|
|
2238
|
+
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Step", value: config.step, placeholder: "1", onChange: (v) => updateConfig({ step: v ?? 1 }) }),
|
|
2239
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Min Label", value: config.minLabel, placeholder: "Low", onChange: (v) => updateConfig({ minLabel: v }) }),
|
|
2240
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Max Label", value: config.maxLabel, placeholder: "High", onChange: (v) => updateConfig({ maxLabel: v }) }),
|
|
2241
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToggleField, { label: "Show Value", checked: config.showValue !== false, onChange: (v) => updateConfig({ showValue: v }) }),
|
|
2242
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Unit", value: config.unit, placeholder: "e.g. %", onChange: (v) => updateConfig({ unit: v }) })
|
|
2199
2243
|
] });
|
|
2200
2244
|
case "rating":
|
|
2201
2245
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Rating Settings", children: [
|
|
2202
|
-
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max
|
|
2203
|
-
/* @__PURE__ */ jsxRuntime.jsx(SelectField, { label: "Icon", value: config.icon ?? "star", options: [{ label: "Star", value: "star" }, { label: "Heart", value: "heart" }, { label: "
|
|
2204
|
-
/* @__PURE__ */ jsxRuntime.jsx(ToggleField, { label: "
|
|
2246
|
+
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Rating", value: config.maxRating, placeholder: "5", onChange: (v) => updateConfig({ maxRating: v ?? 5 }) }),
|
|
2247
|
+
/* @__PURE__ */ jsxRuntime.jsx(SelectField, { label: "Icon", value: config.icon ?? "star", options: [{ label: "Star", value: "star" }, { label: "Heart", value: "heart" }, { label: "Thumb", value: "thumb" }], onChange: (v) => updateConfig({ icon: v }) }),
|
|
2248
|
+
/* @__PURE__ */ jsxRuntime.jsx(ToggleField, { label: "Allow Half Ratings", checked: !!config.allowHalf, onChange: (v) => updateConfig({ allowHalf: v }) })
|
|
2205
2249
|
] });
|
|
2206
2250
|
case "nps":
|
|
2207
2251
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "NPS Settings", children: [
|
|
2208
|
-
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "
|
|
2209
|
-
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "
|
|
2252
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Left Label", value: config.leftLabel ?? "Not likely", onChange: (v) => updateConfig({ leftLabel: v }) }),
|
|
2253
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Right Label", value: config.rightLabel ?? "Extremely likely", onChange: (v) => updateConfig({ rightLabel: v }) })
|
|
2210
2254
|
] });
|
|
2211
2255
|
case "opinion_scale":
|
|
2212
2256
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Scale Settings", children: [
|
|
@@ -2272,50 +2316,126 @@ function QuestionConfigEditor({ question, onUpdate }) {
|
|
|
2272
2316
|
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Range (days)", value: config.maxRangeDays, onChange: (v) => updateConfig({ maxRangeDays: v }) })
|
|
2273
2317
|
] });
|
|
2274
2318
|
case "appointment": {
|
|
2275
|
-
const appointmentMode = typeof config.
|
|
2319
|
+
const appointmentMode = typeof config.slotsUrl === "string" ? "url" : typeof config.embedUrl === "string" ? "embed" : "static";
|
|
2320
|
+
const timezoneFieldOptions = [
|
|
2321
|
+
{ label: "None (Static Timezone)", value: "" },
|
|
2322
|
+
...candidateTimezoneFields,
|
|
2323
|
+
{ label: "Custom Field ID...", value: "__custom__" }
|
|
2324
|
+
];
|
|
2325
|
+
const isCustomTzField = Boolean(config.timezoneField) && !candidateTimezoneFields.some((opt) => opt.value === config.timezoneField);
|
|
2276
2326
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Appointment Settings", children: [
|
|
2277
2327
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2278
2328
|
SelectField,
|
|
2279
2329
|
{
|
|
2280
|
-
label: "Mode",
|
|
2330
|
+
label: "Scheduling Mode",
|
|
2281
2331
|
value: appointmentMode,
|
|
2282
2332
|
options: [
|
|
2283
|
-
{ label: "Static Slots", value: "static" },
|
|
2284
|
-
{ label: "
|
|
2285
|
-
{ label: "Embed (Calendly / Cal.com)", value: "embed" }
|
|
2333
|
+
{ label: "Manual Static Slots", value: "static" },
|
|
2334
|
+
{ label: "Fetch from URL", value: "url" },
|
|
2335
|
+
{ label: "Third-Party Embed (Calendly / Cal.com)", value: "embed" }
|
|
2286
2336
|
],
|
|
2287
2337
|
onChange: (v) => {
|
|
2288
|
-
if (v === "
|
|
2289
|
-
updateConfig({
|
|
2290
|
-
|
|
2291
|
-
|
|
2338
|
+
if (v === "url") {
|
|
2339
|
+
updateConfig({
|
|
2340
|
+
slotsUrl: "",
|
|
2341
|
+
embedUrl: void 0,
|
|
2342
|
+
embedProvider: void 0,
|
|
2343
|
+
slots: void 0
|
|
2344
|
+
});
|
|
2292
2345
|
} else if (v === "embed") {
|
|
2293
|
-
updateConfig({
|
|
2346
|
+
updateConfig({
|
|
2347
|
+
embedUrl: "",
|
|
2348
|
+
embedProvider: config.embedProvider || "cal_com",
|
|
2349
|
+
slotsUrl: void 0,
|
|
2350
|
+
slots: void 0
|
|
2351
|
+
});
|
|
2352
|
+
} else {
|
|
2353
|
+
updateConfig({
|
|
2354
|
+
slots: config.slots ?? [],
|
|
2355
|
+
slotsUrl: void 0,
|
|
2356
|
+
embedUrl: void 0,
|
|
2357
|
+
embedProvider: void 0
|
|
2358
|
+
});
|
|
2294
2359
|
}
|
|
2295
2360
|
}
|
|
2296
2361
|
}
|
|
2297
2362
|
),
|
|
2298
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2363
|
+
appointmentMode === "url" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2364
|
+
TextField,
|
|
2365
|
+
{
|
|
2366
|
+
label: "Slots URL",
|
|
2367
|
+
value: config.slotsUrl,
|
|
2368
|
+
placeholder: "https://example.com/slots",
|
|
2369
|
+
onChange: (v) => updateConfig({ slotsUrl: v ?? "" })
|
|
2370
|
+
}
|
|
2371
|
+
),
|
|
2303
2372
|
appointmentMode === "embed" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2304
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2373
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2374
|
+
TextField,
|
|
2375
|
+
{
|
|
2376
|
+
label: "Embed URL",
|
|
2377
|
+
value: config.embedUrl,
|
|
2378
|
+
placeholder: "https://calendly.com/... or https://cal.com/...",
|
|
2379
|
+
onChange: (v) => updateConfig({ embedUrl: v ?? "" })
|
|
2380
|
+
}
|
|
2381
|
+
),
|
|
2305
2382
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2306
2383
|
SelectField,
|
|
2307
2384
|
{
|
|
2308
2385
|
label: "Embed Provider",
|
|
2309
|
-
value: config.embedProvider ?? "
|
|
2386
|
+
value: config.embedProvider ?? "cal_com",
|
|
2310
2387
|
options: [
|
|
2311
|
-
{ label: "Calendly", value: "calendly" },
|
|
2312
2388
|
{ label: "Cal.com", value: "cal_com" },
|
|
2389
|
+
{ label: "Calendly", value: "calendly" },
|
|
2313
2390
|
{ label: "Custom", value: "custom" }
|
|
2314
2391
|
],
|
|
2315
2392
|
onChange: (v) => updateConfig({ embedProvider: v })
|
|
2316
2393
|
}
|
|
2317
2394
|
)
|
|
2318
2395
|
] }),
|
|
2396
|
+
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Duration (min)", value: config.duration ?? 30, onChange: (v) => updateConfig({ duration: v }) }),
|
|
2397
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Timezone", value: config.timezone, placeholder: "e.g. America/New_York", onChange: (v) => updateConfig({ timezone: v }) }),
|
|
2398
|
+
candidateTimezoneFields.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2399
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2400
|
+
SelectField,
|
|
2401
|
+
{
|
|
2402
|
+
label: "Dynamic Timezone Field",
|
|
2403
|
+
value: isCustomTzField ? "__custom__" : config.timezoneField ?? "",
|
|
2404
|
+
options: timezoneFieldOptions,
|
|
2405
|
+
onChange: (v) => {
|
|
2406
|
+
if (v !== "__custom__") {
|
|
2407
|
+
updateConfig({ timezoneField: v || void 0 });
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
),
|
|
2412
|
+
isCustomTzField && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2413
|
+
TextField,
|
|
2414
|
+
{
|
|
2415
|
+
label: "Custom Timezone Field ID",
|
|
2416
|
+
value: config.timezoneField,
|
|
2417
|
+
placeholder: "e.g. timezone",
|
|
2418
|
+
onChange: (v) => updateConfig({ timezoneField: v || void 0 })
|
|
2419
|
+
}
|
|
2420
|
+
) })
|
|
2421
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2422
|
+
TextField,
|
|
2423
|
+
{
|
|
2424
|
+
label: "Timezone Field",
|
|
2425
|
+
value: config.timezoneField,
|
|
2426
|
+
placeholder: "Field ID for dynamic timezone",
|
|
2427
|
+
onChange: (v) => updateConfig({ timezoneField: v || void 0 })
|
|
2428
|
+
}
|
|
2429
|
+
),
|
|
2430
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Date Format", value: config.dateFormat, placeholder: "Locale-aware (default)", onChange: (v) => updateConfig({ dateFormat: v }) }),
|
|
2431
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2432
|
+
ToggleField,
|
|
2433
|
+
{
|
|
2434
|
+
label: "Auto-Advance on Booking Complete",
|
|
2435
|
+
checked: config.autoAdvance !== false,
|
|
2436
|
+
onChange: (v) => updateConfig({ autoAdvance: v })
|
|
2437
|
+
}
|
|
2438
|
+
),
|
|
2319
2439
|
appointmentMode === "static" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2320
2440
|
AppointmentSlotsEditor,
|
|
2321
2441
|
{
|
|
@@ -2328,10 +2448,41 @@ function QuestionConfigEditor({ question, onUpdate }) {
|
|
|
2328
2448
|
// ── Media ──
|
|
2329
2449
|
case "file_upload":
|
|
2330
2450
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Upload Settings", children: [
|
|
2451
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-2.5 bg-muted/40 border border-border rounded-md text-xs text-muted-foreground flex items-center gap-1.5", children: [
|
|
2452
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u{1F4E6}" }),
|
|
2453
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Uploads stream to your cloud bucket using Form / Account Storage settings." })
|
|
2454
|
+
] }),
|
|
2331
2455
|
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Files", value: config.maxFiles ?? 1, onChange: (v) => updateConfig({ maxFiles: v }) }),
|
|
2332
2456
|
/* @__PURE__ */ jsxRuntime.jsx(NumberField, { label: "Max Size (MB)", value: config.maxSizeMb ?? 10, onChange: (v) => updateConfig({ maxSizeMb: v }) }),
|
|
2333
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2334
|
-
|
|
2457
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2458
|
+
TextField,
|
|
2459
|
+
{
|
|
2460
|
+
label: "Accepted Types (comma-separated)",
|
|
2461
|
+
value: config.accept?.join(", "),
|
|
2462
|
+
placeholder: "e.g. image/*, .pdf, .docx, .zip",
|
|
2463
|
+
onChange: (v) => updateConfig({
|
|
2464
|
+
accept: v ? v.split(",").map((s) => s.trim()).filter(Boolean) : void 0
|
|
2465
|
+
})
|
|
2466
|
+
}
|
|
2467
|
+
),
|
|
2468
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2469
|
+
TextField,
|
|
2470
|
+
{
|
|
2471
|
+
label: "Upload URL",
|
|
2472
|
+
value: config.uploadUrl,
|
|
2473
|
+
placeholder: "https://api.example.com/s3/presign",
|
|
2474
|
+
onChange: (v) => updateConfig({ uploadUrl: v || void 0 })
|
|
2475
|
+
}
|
|
2476
|
+
),
|
|
2477
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2478
|
+
TextField,
|
|
2479
|
+
{
|
|
2480
|
+
label: "Public Key",
|
|
2481
|
+
value: config.publicKey,
|
|
2482
|
+
placeholder: "pk_test_...",
|
|
2483
|
+
onChange: (v) => updateConfig({ publicKey: v || void 0 })
|
|
2484
|
+
}
|
|
2485
|
+
)
|
|
2335
2486
|
] });
|
|
2336
2487
|
case "signature":
|
|
2337
2488
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Signature Settings", children: [
|
|
@@ -2448,32 +2599,120 @@ function QuestionConfigEditor({ question, onUpdate }) {
|
|
|
2448
2599
|
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Default Country", value: config.defaultCountry, placeholder: "e.g. US", onChange: (v) => updateConfig({ defaultCountry: v }) }),
|
|
2449
2600
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2450
2601
|
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Label, { className: "text-xs text-muted-foreground mb-2 block", children: "Fields" }),
|
|
2451
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: ADDRESS_FIELDS.map((f) =>
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2602
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: ADDRESS_FIELDS.map((f) => {
|
|
2603
|
+
const isChecked = activeFields.includes(f.value);
|
|
2604
|
+
const isLastChecked = isChecked && activeFields.length === 1;
|
|
2605
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2606
|
+
ToggleField,
|
|
2607
|
+
{
|
|
2608
|
+
label: f.label,
|
|
2609
|
+
checked: isChecked,
|
|
2610
|
+
disabled: isLastChecked,
|
|
2611
|
+
onChange: (checked) => {
|
|
2612
|
+
const next = checked ? [...activeFields, f.value] : activeFields.filter((v) => v !== f.value);
|
|
2613
|
+
updateConfig({ fields: next });
|
|
2614
|
+
}
|
|
2615
|
+
},
|
|
2616
|
+
f.value
|
|
2617
|
+
);
|
|
2618
|
+
}) })
|
|
2463
2619
|
] })
|
|
2464
2620
|
] });
|
|
2465
2621
|
}
|
|
2466
|
-
case "payment":
|
|
2622
|
+
case "payment": {
|
|
2623
|
+
const amountFieldOptions = [
|
|
2624
|
+
{ label: "None (Fixed Amount)", value: "" },
|
|
2625
|
+
...candidateAmountFields,
|
|
2626
|
+
{ label: "Custom Field ID...", value: "__custom__" }
|
|
2627
|
+
];
|
|
2628
|
+
const isCustomAmountField = Boolean(config.amountField) && !candidateAmountFields.some((opt) => opt.value === config.amountField);
|
|
2467
2629
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Payment Settings", children: [
|
|
2468
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2630
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2631
|
+
SelectField,
|
|
2632
|
+
{
|
|
2633
|
+
label: "Provider",
|
|
2634
|
+
value: config.provider ?? "stripe",
|
|
2635
|
+
options: [
|
|
2636
|
+
{ label: "Stripe", value: "stripe" },
|
|
2637
|
+
{ label: "PayPal", value: "paypal" },
|
|
2638
|
+
{ label: "Razorpay", value: "razorpay" }
|
|
2639
|
+
],
|
|
2640
|
+
onChange: (v) => updateConfig({ provider: v })
|
|
2641
|
+
}
|
|
2642
|
+
),
|
|
2643
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2644
|
+
TextField,
|
|
2645
|
+
{
|
|
2646
|
+
label: "Public Key",
|
|
2647
|
+
value: config.publicKey,
|
|
2648
|
+
placeholder: "pk_test_...",
|
|
2649
|
+
onChange: (v) => updateConfig({ publicKey: v || void 0 })
|
|
2650
|
+
}
|
|
2651
|
+
),
|
|
2652
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2653
|
+
TextField,
|
|
2654
|
+
{
|
|
2655
|
+
label: "Server URL",
|
|
2656
|
+
value: config.serverUrl,
|
|
2657
|
+
placeholder: "https://api.example.com/create-intent",
|
|
2658
|
+
onChange: (v) => updateConfig({ serverUrl: v || void 0 })
|
|
2659
|
+
}
|
|
2660
|
+
),
|
|
2661
|
+
candidateAmountFields.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2662
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2663
|
+
SelectField,
|
|
2664
|
+
{
|
|
2665
|
+
label: "Dynamic Amount Field",
|
|
2666
|
+
value: isCustomAmountField ? "__custom__" : config.amountField ?? "",
|
|
2667
|
+
options: amountFieldOptions,
|
|
2668
|
+
onChange: (v) => {
|
|
2669
|
+
if (v !== "__custom__") {
|
|
2670
|
+
updateConfig({ amountField: v || void 0 });
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
),
|
|
2675
|
+
isCustomAmountField && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2676
|
+
TextField,
|
|
2677
|
+
{
|
|
2678
|
+
label: "Custom Field ID",
|
|
2679
|
+
value: config.amountField,
|
|
2680
|
+
placeholder: "Field ID for dynamic amount",
|
|
2681
|
+
onChange: (v) => updateConfig({ amountField: v || void 0 })
|
|
2682
|
+
}
|
|
2683
|
+
) })
|
|
2684
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2685
|
+
TextField,
|
|
2686
|
+
{
|
|
2687
|
+
label: "Amount Field",
|
|
2688
|
+
value: config.amountField,
|
|
2689
|
+
placeholder: "Field ID for dynamic amount (e.g. total_cost)",
|
|
2690
|
+
onChange: (v) => updateConfig({ amountField: v || void 0 })
|
|
2691
|
+
}
|
|
2692
|
+
),
|
|
2693
|
+
!config.amountField && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2694
|
+
NumberField,
|
|
2695
|
+
{
|
|
2696
|
+
label: "Fixed Amount",
|
|
2697
|
+
value: config.amount,
|
|
2698
|
+
onChange: (v) => updateConfig({ amount: v })
|
|
2699
|
+
}
|
|
2700
|
+
),
|
|
2701
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Currency", value: config.currency ?? "USD", placeholder: "USD", onChange: (v) => updateConfig({ currency: v || "USD" }) }),
|
|
2702
|
+
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Description", value: config.description, onChange: (v) => updateConfig({ description: v }) }),
|
|
2703
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2704
|
+
TextField,
|
|
2705
|
+
{
|
|
2706
|
+
label: "Client Secret Response Path",
|
|
2707
|
+
value: typeof config.responseMapping?.clientSecretPath === "string" ? config.responseMapping.clientSecretPath : config.responseMapping ?? "",
|
|
2708
|
+
placeholder: "clientSecret",
|
|
2709
|
+
onChange: (v) => updateConfig({
|
|
2710
|
+
responseMapping: v ? { clientSecretPath: v } : void 0
|
|
2711
|
+
})
|
|
2712
|
+
}
|
|
2713
|
+
)
|
|
2476
2714
|
] });
|
|
2715
|
+
}
|
|
2477
2716
|
case "calculated":
|
|
2478
2717
|
return /* @__PURE__ */ jsxRuntime.jsxs(ConfigSection, { title: "Calculated Field", children: [
|
|
2479
2718
|
/* @__PURE__ */ jsxRuntime.jsx(TextField, { label: "Expression", value: config.expression ?? "", placeholder: "e.g. {q1} + {q2}", onChange: (v) => updateConfig({ expression: v }) }),
|
|
@@ -2582,11 +2821,12 @@ function NumberField({
|
|
|
2582
2821
|
function ToggleField({
|
|
2583
2822
|
label,
|
|
2584
2823
|
checked,
|
|
2585
|
-
onChange
|
|
2824
|
+
onChange,
|
|
2825
|
+
disabled
|
|
2586
2826
|
}) {
|
|
2587
2827
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2588
2828
|
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Label, { className: "text-xs text-muted-foreground", children: label }),
|
|
2589
|
-
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Switch, { checked, onCheckedChange: onChange })
|
|
2829
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Switch, { checked, onCheckedChange: onChange, disabled })
|
|
2590
2830
|
] });
|
|
2591
2831
|
}
|
|
2592
2832
|
function SelectField({
|
|
@@ -3558,7 +3798,14 @@ function QuestionProperties({ question, sectionId, builderState, onClose, onOpen
|
|
|
3558
3798
|
}
|
|
3559
3799
|
)
|
|
3560
3800
|
] }),
|
|
3561
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3801
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3802
|
+
QuestionConfigEditor,
|
|
3803
|
+
{
|
|
3804
|
+
question,
|
|
3805
|
+
schema: builderState.schema,
|
|
3806
|
+
onUpdate: updateQuestion2
|
|
3807
|
+
}
|
|
3808
|
+
),
|
|
3562
3809
|
hasOptions && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3563
3810
|
OptionsEditor,
|
|
3564
3811
|
{
|
|
@@ -3735,6 +3982,11 @@ function MonacoWrapper({ value, onChange, errors }) {
|
|
|
3735
3982
|
editorRef.current = editor;
|
|
3736
3983
|
monacoRef.current = monaco;
|
|
3737
3984
|
};
|
|
3985
|
+
React.useEffect(() => {
|
|
3986
|
+
return () => {
|
|
3987
|
+
editorRef.current?.dispose();
|
|
3988
|
+
};
|
|
3989
|
+
}, []);
|
|
3738
3990
|
React.useEffect(() => {
|
|
3739
3991
|
const editor = editorRef.current;
|
|
3740
3992
|
const monaco = monacoRef.current;
|
|
@@ -4539,6 +4791,11 @@ function TemplateGallery({ templates, onSelect, onClose }) {
|
|
|
4539
4791
|
function useDebouncedValidation(delayMs = 500) {
|
|
4540
4792
|
const [errors, setErrors] = React.useState([]);
|
|
4541
4793
|
const timerRef = React.useRef(null);
|
|
4794
|
+
React.useEffect(() => {
|
|
4795
|
+
return () => {
|
|
4796
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
4797
|
+
};
|
|
4798
|
+
}, []);
|
|
4542
4799
|
const validate = React.useCallback(
|
|
4543
4800
|
(text) => {
|
|
4544
4801
|
if (timerRef.current) {
|
|
@@ -5232,62 +5489,1143 @@ ${details}`);
|
|
|
5232
5489
|
showTemplateGallery && templates && /* @__PURE__ */ jsxRuntime.jsx(
|
|
5233
5490
|
TemplateGallery,
|
|
5234
5491
|
{
|
|
5235
|
-
templates,
|
|
5236
|
-
onSelect: (template) => builderState.resetSchema(template.schema),
|
|
5237
|
-
onClose: () => setShowTemplateGallery(false)
|
|
5492
|
+
templates,
|
|
5493
|
+
onSelect: (template) => builderState.resetSchema(template.schema),
|
|
5494
|
+
onClose: () => setShowTemplateGallery(false)
|
|
5495
|
+
}
|
|
5496
|
+
)
|
|
5497
|
+
]
|
|
5498
|
+
}
|
|
5499
|
+
) });
|
|
5500
|
+
}
|
|
5501
|
+
function DragOverlayContent({
|
|
5502
|
+
item,
|
|
5503
|
+
schema,
|
|
5504
|
+
questionTypes: types
|
|
5505
|
+
}) {
|
|
5506
|
+
if (item.type === "palette-item") {
|
|
5507
|
+
const typeInfo = types[item.questionType];
|
|
5508
|
+
if (!typeInfo) return null;
|
|
5509
|
+
const IconComponent = getIcon(typeInfo.icon);
|
|
5510
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2 rounded-md border border-primary bg-card text-foreground text-sm fcb-shadow-lg cursor-grabbing", children: [
|
|
5511
|
+
/* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 14, className: "shrink-0 text-primary", strokeWidth: 1.75 }),
|
|
5512
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: typeInfo.label })
|
|
5513
|
+
] });
|
|
5514
|
+
}
|
|
5515
|
+
if (item.type === "question") {
|
|
5516
|
+
const found = findQuestion(schema, item.sectionId, item.questionId);
|
|
5517
|
+
if (!found) return null;
|
|
5518
|
+
const typeInfo = types[found.question.type];
|
|
5519
|
+
const IconComponent = typeInfo ? getIcon(typeInfo.icon) : null;
|
|
5520
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
5521
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 mb-1", children: /* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Badge, { variant: "secondary", className: "gap-1.5 text-muted-foreground", children: [
|
|
5522
|
+
IconComponent && /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 11, strokeWidth: 1.75 }),
|
|
5523
|
+
typeInfo?.label ?? found.question.type
|
|
5524
|
+
] }) }),
|
|
5525
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: found.question.label })
|
|
5526
|
+
] });
|
|
5527
|
+
}
|
|
5528
|
+
if (item.type === "section") {
|
|
5529
|
+
const section = schema.sections.find((s) => s.id === item.sectionId);
|
|
5530
|
+
if (!section) return null;
|
|
5531
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
5532
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold", children: section.title }),
|
|
5533
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: [
|
|
5534
|
+
section.questions.length,
|
|
5535
|
+
" field",
|
|
5536
|
+
section.questions.length !== 1 ? "s" : ""
|
|
5537
|
+
] })
|
|
5538
|
+
] });
|
|
5539
|
+
}
|
|
5540
|
+
return null;
|
|
5541
|
+
}
|
|
5542
|
+
function FormBuilderInner(props) {
|
|
5543
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(FormBuilderCore, { ...props }) });
|
|
5544
|
+
}
|
|
5545
|
+
|
|
5546
|
+
// src/form-builder/components/FormBuilderGated.tsx
|
|
5547
|
+
var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
|
|
5548
|
+
function formatBytes(bytes, decimals = 1) {
|
|
5549
|
+
if (bytes === 0) return "0 B";
|
|
5550
|
+
const k = 1024;
|
|
5551
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
5552
|
+
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
5553
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
5554
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
5555
|
+
}
|
|
5556
|
+
function isFileAccepted(file, acceptList) {
|
|
5557
|
+
if (!acceptList || acceptList.length === 0) return true;
|
|
5558
|
+
const fileName = file.name.toLowerCase();
|
|
5559
|
+
const fileType = file.type.toLowerCase();
|
|
5560
|
+
return acceptList.some((pattern) => {
|
|
5561
|
+
const p = pattern.trim().toLowerCase();
|
|
5562
|
+
if (!p) return false;
|
|
5563
|
+
if (p === "*/*") return true;
|
|
5564
|
+
if (p.startsWith(".")) {
|
|
5565
|
+
return fileName.endsWith(p);
|
|
5566
|
+
}
|
|
5567
|
+
if (p.endsWith("/*")) {
|
|
5568
|
+
const baseType = p.slice(0, -2);
|
|
5569
|
+
return fileType.startsWith(baseType);
|
|
5570
|
+
}
|
|
5571
|
+
return fileType === p;
|
|
5572
|
+
});
|
|
5573
|
+
}
|
|
5574
|
+
async function decipherPresignPayload(encryptedHex, ivHex, secretKeyStr) {
|
|
5575
|
+
const enc = new TextEncoder();
|
|
5576
|
+
const rawKey = enc.encode(secretKeyStr.padEnd(32, "0").slice(0, 32));
|
|
5577
|
+
const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
|
|
5578
|
+
if (!cryptoObj || !cryptoObj.subtle) {
|
|
5579
|
+
throw new Error("Web Crypto API is not supported in this environment.");
|
|
5580
|
+
}
|
|
5581
|
+
const cryptoKey = await cryptoObj.subtle.importKey(
|
|
5582
|
+
"raw",
|
|
5583
|
+
rawKey,
|
|
5584
|
+
{ name: "AES-GCM" },
|
|
5585
|
+
false,
|
|
5586
|
+
["decrypt"]
|
|
5587
|
+
);
|
|
5588
|
+
let iv;
|
|
5589
|
+
if (/^[0-9a-fA-F]+$/.test(ivHex) && ivHex.length % 2 === 0) {
|
|
5590
|
+
iv = new Uint8Array(ivHex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
5591
|
+
} else {
|
|
5592
|
+
iv = Uint8Array.from(atob(ivHex), (c) => c.charCodeAt(0));
|
|
5593
|
+
}
|
|
5594
|
+
let ciphertext;
|
|
5595
|
+
if (/^[0-9a-fA-F]+$/.test(encryptedHex) && encryptedHex.length % 2 === 0) {
|
|
5596
|
+
ciphertext = new Uint8Array(encryptedHex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
5597
|
+
} else {
|
|
5598
|
+
ciphertext = Uint8Array.from(atob(encryptedHex), (c) => c.charCodeAt(0));
|
|
5599
|
+
}
|
|
5600
|
+
const decryptedBuffer = await cryptoObj.subtle.decrypt(
|
|
5601
|
+
{ name: "AES-GCM", iv },
|
|
5602
|
+
cryptoKey,
|
|
5603
|
+
ciphertext
|
|
5604
|
+
);
|
|
5605
|
+
const plaintext = new TextDecoder().decode(decryptedBuffer);
|
|
5606
|
+
return JSON.parse(plaintext);
|
|
5607
|
+
}
|
|
5608
|
+
function bytesToHex(bytes) {
|
|
5609
|
+
let hex = "";
|
|
5610
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
5611
|
+
hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
|
|
5612
|
+
}
|
|
5613
|
+
return hex;
|
|
5614
|
+
}
|
|
5615
|
+
async function generateEphemeralKeyPair() {
|
|
5616
|
+
const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
|
|
5617
|
+
if (!cryptoObj || !cryptoObj.subtle) {
|
|
5618
|
+
throw new Error("Web Crypto API is not supported in this environment.");
|
|
5619
|
+
}
|
|
5620
|
+
return await cryptoObj.subtle.generateKey(
|
|
5621
|
+
{
|
|
5622
|
+
name: "RSA-OAEP",
|
|
5623
|
+
modulusLength: 2048,
|
|
5624
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
5625
|
+
hash: "SHA-256"
|
|
5626
|
+
},
|
|
5627
|
+
true,
|
|
5628
|
+
["encrypt", "decrypt"]
|
|
5629
|
+
);
|
|
5630
|
+
}
|
|
5631
|
+
async function exportPublicKeySpki(publicKey) {
|
|
5632
|
+
const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
|
|
5633
|
+
const exported = await cryptoObj.subtle.exportKey("spki", publicKey);
|
|
5634
|
+
const bytes = new Uint8Array(exported);
|
|
5635
|
+
let binary = "";
|
|
5636
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
5637
|
+
binary += String.fromCharCode(bytes[i] ?? 0);
|
|
5638
|
+
}
|
|
5639
|
+
return btoa(binary);
|
|
5640
|
+
}
|
|
5641
|
+
async function decryptEphemeralPayload(payload, privateKey) {
|
|
5642
|
+
const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
|
|
5643
|
+
const cipher = payload.encryptedPayload || payload.ciphertext;
|
|
5644
|
+
if (!cipher) throw new Error("Missing encryptedPayload in response");
|
|
5645
|
+
let ciphertext;
|
|
5646
|
+
if (/^[0-9a-fA-F]+$/.test(cipher) && cipher.length % 2 === 0) {
|
|
5647
|
+
ciphertext = new Uint8Array(cipher.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
5648
|
+
} else {
|
|
5649
|
+
ciphertext = Uint8Array.from(atob(cipher), (c) => c.charCodeAt(0));
|
|
5650
|
+
}
|
|
5651
|
+
if (payload.encryptedKey && payload.iv) {
|
|
5652
|
+
let encKeyBytes;
|
|
5653
|
+
if (/^[0-9a-fA-F]+$/.test(payload.encryptedKey) && payload.encryptedKey.length % 2 === 0) {
|
|
5654
|
+
encKeyBytes = new Uint8Array(payload.encryptedKey.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
5655
|
+
} else {
|
|
5656
|
+
encKeyBytes = Uint8Array.from(atob(payload.encryptedKey), (c) => c.charCodeAt(0));
|
|
5657
|
+
}
|
|
5658
|
+
const rawAesKey = await cryptoObj.subtle.decrypt(
|
|
5659
|
+
{ name: "RSA-OAEP" },
|
|
5660
|
+
privateKey,
|
|
5661
|
+
encKeyBytes
|
|
5662
|
+
);
|
|
5663
|
+
const aesKey = await cryptoObj.subtle.importKey(
|
|
5664
|
+
"raw",
|
|
5665
|
+
rawAesKey,
|
|
5666
|
+
{ name: "AES-GCM" },
|
|
5667
|
+
false,
|
|
5668
|
+
["decrypt"]
|
|
5669
|
+
);
|
|
5670
|
+
let iv;
|
|
5671
|
+
if (/^[0-9a-fA-F]+$/.test(payload.iv) && payload.iv.length % 2 === 0) {
|
|
5672
|
+
iv = new Uint8Array(payload.iv.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
5673
|
+
} else {
|
|
5674
|
+
iv = Uint8Array.from(atob(payload.iv), (c) => c.charCodeAt(0));
|
|
5675
|
+
}
|
|
5676
|
+
const decrypted2 = await cryptoObj.subtle.decrypt(
|
|
5677
|
+
{ name: "AES-GCM", iv },
|
|
5678
|
+
aesKey,
|
|
5679
|
+
ciphertext
|
|
5680
|
+
);
|
|
5681
|
+
return JSON.parse(new TextDecoder().decode(decrypted2));
|
|
5682
|
+
}
|
|
5683
|
+
const decrypted = await cryptoObj.subtle.decrypt(
|
|
5684
|
+
{ name: "RSA-OAEP" },
|
|
5685
|
+
privateKey,
|
|
5686
|
+
ciphertext
|
|
5687
|
+
);
|
|
5688
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
5689
|
+
}
|
|
5690
|
+
async function encryptPresignPayload(data, secretKeyStr) {
|
|
5691
|
+
const enc = new TextEncoder();
|
|
5692
|
+
const rawKey = enc.encode(secretKeyStr.padEnd(32, "0").slice(0, 32));
|
|
5693
|
+
const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
|
|
5694
|
+
if (!cryptoObj || !cryptoObj.subtle) {
|
|
5695
|
+
throw new Error("Web Crypto API is not supported in this environment.");
|
|
5696
|
+
}
|
|
5697
|
+
const cryptoKey = await cryptoObj.subtle.importKey(
|
|
5698
|
+
"raw",
|
|
5699
|
+
rawKey,
|
|
5700
|
+
{ name: "AES-GCM" },
|
|
5701
|
+
false,
|
|
5702
|
+
["encrypt"]
|
|
5703
|
+
);
|
|
5704
|
+
const iv = cryptoObj.getRandomValues(new Uint8Array(12));
|
|
5705
|
+
const plaintextBytes = enc.encode(JSON.stringify(data));
|
|
5706
|
+
const encryptedBuffer = await cryptoObj.subtle.decrypt ? await cryptoObj.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, plaintextBytes) : new ArrayBuffer(0);
|
|
5707
|
+
const encryptedPayload = bytesToHex(new Uint8Array(encryptedBuffer));
|
|
5708
|
+
const ivHex = bytesToHex(iv);
|
|
5709
|
+
return { encryptedPayload, iv: ivHex };
|
|
5710
|
+
}
|
|
5711
|
+
function ProFileUploadField(props) {
|
|
5712
|
+
const {
|
|
5713
|
+
value,
|
|
5714
|
+
onChange,
|
|
5715
|
+
onBlur,
|
|
5716
|
+
error,
|
|
5717
|
+
touched,
|
|
5718
|
+
disabled = false,
|
|
5719
|
+
customProps
|
|
5720
|
+
} = props;
|
|
5721
|
+
const field = props.field || props.question;
|
|
5722
|
+
const config = field?.config ?? {};
|
|
5723
|
+
const maxFiles = config.maxFiles ?? 1;
|
|
5724
|
+
const maxSizeMb = config.maxSizeMb ?? 10;
|
|
5725
|
+
const maxSizeBytes = maxSizeMb * 1024 * 1024;
|
|
5726
|
+
const acceptList = config.accept;
|
|
5727
|
+
const [items, setItems] = React.useState(() => {
|
|
5728
|
+
if (!value) return [];
|
|
5729
|
+
const arrayVal = Array.isArray(value) ? value : [value];
|
|
5730
|
+
return arrayVal.filter((v) => Boolean(v && typeof v === "object" && "url" in v)).map((v, idx) => ({
|
|
5731
|
+
id: `init-${idx}-${v.name}`,
|
|
5732
|
+
name: v.name,
|
|
5733
|
+
size: v.size || 0,
|
|
5734
|
+
type: v.type || "application/octet-stream",
|
|
5735
|
+
progress: 100,
|
|
5736
|
+
status: "succeeded",
|
|
5737
|
+
url: v.url,
|
|
5738
|
+
key: v.key
|
|
5739
|
+
}));
|
|
5740
|
+
});
|
|
5741
|
+
const [isDragOver, setIsDragOver] = React.useState(false);
|
|
5742
|
+
const [generalError, setGeneralError] = React.useState(null);
|
|
5743
|
+
const fileInputRef = React.useRef(null);
|
|
5744
|
+
const activeXhrsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
5745
|
+
const notifyChange = React.useCallback(
|
|
5746
|
+
(newItems) => {
|
|
5747
|
+
const succeeded = newItems.filter((item) => item.status === "succeeded" && item.url).map((item) => ({
|
|
5748
|
+
name: item.name,
|
|
5749
|
+
size: item.size,
|
|
5750
|
+
type: item.type,
|
|
5751
|
+
url: item.url,
|
|
5752
|
+
key: item.key,
|
|
5753
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5754
|
+
}));
|
|
5755
|
+
if (maxFiles === 1) {
|
|
5756
|
+
onChange(succeeded.length > 0 ? succeeded[0] : null);
|
|
5757
|
+
} else {
|
|
5758
|
+
onChange(succeeded);
|
|
5759
|
+
}
|
|
5760
|
+
},
|
|
5761
|
+
[maxFiles, onChange]
|
|
5762
|
+
);
|
|
5763
|
+
React.useEffect(() => {
|
|
5764
|
+
return () => {
|
|
5765
|
+
activeXhrsRef.current.forEach((xhr) => xhr.abort());
|
|
5766
|
+
activeXhrsRef.current.clear();
|
|
5767
|
+
};
|
|
5768
|
+
}, []);
|
|
5769
|
+
const uploadSingleFile = React.useCallback(
|
|
5770
|
+
async (fileState) => {
|
|
5771
|
+
const { file, id } = fileState;
|
|
5772
|
+
if (!file) return;
|
|
5773
|
+
const uploadEndpoint = config.uploadUrl || customProps?.uploadUrl || customProps?.presignUrl;
|
|
5774
|
+
if (!uploadEndpoint) {
|
|
5775
|
+
setItems((prev) => {
|
|
5776
|
+
const next = prev.map(
|
|
5777
|
+
(item) => item.id === id ? {
|
|
5778
|
+
...item,
|
|
5779
|
+
progress: 0,
|
|
5780
|
+
status: "error",
|
|
5781
|
+
error: "No upload endpoint configured. Please configure storage settings or pass an uploadUrl."
|
|
5782
|
+
} : item
|
|
5783
|
+
);
|
|
5784
|
+
setTimeout(() => notifyChange(next), 0);
|
|
5785
|
+
return next;
|
|
5786
|
+
});
|
|
5787
|
+
return;
|
|
5788
|
+
}
|
|
5789
|
+
try {
|
|
5790
|
+
let ephemeralKeyPair = null;
|
|
5791
|
+
let ephemeralPublicKeyBase64 = void 0;
|
|
5792
|
+
try {
|
|
5793
|
+
const cryptoObj = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
|
|
5794
|
+
if (cryptoObj?.subtle) {
|
|
5795
|
+
ephemeralKeyPair = await generateEphemeralKeyPair();
|
|
5796
|
+
ephemeralPublicKeyBase64 = await exportPublicKeySpki(ephemeralKeyPair.publicKey);
|
|
5797
|
+
}
|
|
5798
|
+
} catch {
|
|
5799
|
+
}
|
|
5800
|
+
const headers = {
|
|
5801
|
+
"Content-Type": "application/json",
|
|
5802
|
+
...ephemeralPublicKeyBase64 ? { "X-Ephemeral-Public-Key": ephemeralPublicKeyBase64 } : {},
|
|
5803
|
+
...config.publicKey ? { "X-Public-Key": config.publicKey } : {},
|
|
5804
|
+
...config.headers || {}
|
|
5805
|
+
};
|
|
5806
|
+
const presignRes = await fetch(uploadEndpoint, {
|
|
5807
|
+
method: "POST",
|
|
5808
|
+
headers,
|
|
5809
|
+
body: JSON.stringify({
|
|
5810
|
+
filename: file.name,
|
|
5811
|
+
contentType: file.type || "application/octet-stream",
|
|
5812
|
+
size: file.size,
|
|
5813
|
+
ephemeralPublicKey: ephemeralPublicKeyBase64,
|
|
5814
|
+
publicKey: config.publicKey,
|
|
5815
|
+
storageProvider: config.storageProvider
|
|
5816
|
+
})
|
|
5817
|
+
});
|
|
5818
|
+
if (!presignRes.ok) {
|
|
5819
|
+
const errData = await presignRes.json().catch(() => null);
|
|
5820
|
+
throw new Error(
|
|
5821
|
+
errData?.message || `Presign request failed with status ${presignRes.status}`
|
|
5822
|
+
);
|
|
5823
|
+
}
|
|
5824
|
+
let presignData = await presignRes.json();
|
|
5825
|
+
if (ephemeralKeyPair && (presignData.encryptedPayload || presignData.ciphertext)) {
|
|
5826
|
+
presignData = await decryptEphemeralPayload(presignData, ephemeralKeyPair.privateKey);
|
|
5827
|
+
} else if (presignData.encryptedPayload || presignData.ciphertext) {
|
|
5828
|
+
const cipher = presignData.encryptedPayload || presignData.ciphertext;
|
|
5829
|
+
const iv = presignData.iv || presignData.nonce;
|
|
5830
|
+
const key2 = config.publicKey || customProps?.publicKey;
|
|
5831
|
+
if (key2) {
|
|
5832
|
+
presignData = await decipherPresignPayload(cipher, iv, key2);
|
|
5833
|
+
} else {
|
|
5834
|
+
throw new Error(
|
|
5835
|
+
"Cannot decipher encrypted upload payload: Missing public key in question configuration."
|
|
5836
|
+
);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
const uploadUrl = presignData.uploadUrl || presignData.url || presignData.signedUrl;
|
|
5840
|
+
const fileUrl = presignData.fileUrl || presignData.publicUrl || uploadUrl.split("?")[0];
|
|
5841
|
+
const key = presignData.key || presignData.fileKey;
|
|
5842
|
+
if (!uploadUrl) {
|
|
5843
|
+
throw new Error("Presign response did not contain an uploadUrl");
|
|
5844
|
+
}
|
|
5845
|
+
const xhr = new XMLHttpRequest();
|
|
5846
|
+
activeXhrsRef.current.set(id, xhr);
|
|
5847
|
+
xhr.upload.onprogress = (e) => {
|
|
5848
|
+
if (e.lengthComputable) {
|
|
5849
|
+
const pct = Math.round(e.loaded / e.total * 100);
|
|
5850
|
+
setItems(
|
|
5851
|
+
(prev) => prev.map((item) => item.id === id ? { ...item, progress: pct } : item)
|
|
5852
|
+
);
|
|
5853
|
+
}
|
|
5854
|
+
};
|
|
5855
|
+
xhr.onload = () => {
|
|
5856
|
+
activeXhrsRef.current.delete(id);
|
|
5857
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
5858
|
+
let finalUrl = fileUrl;
|
|
5859
|
+
let finalKey = key;
|
|
5860
|
+
try {
|
|
5861
|
+
if (xhr.responseText) {
|
|
5862
|
+
const resp = JSON.parse(xhr.responseText);
|
|
5863
|
+
if (resp.secure_url) finalUrl = resp.secure_url;
|
|
5864
|
+
else if (resp.url && !resp.url.includes("?")) finalUrl = resp.url;
|
|
5865
|
+
if (resp.public_id) finalKey = resp.public_id;
|
|
5866
|
+
else if (resp.key) finalKey = resp.key;
|
|
5867
|
+
}
|
|
5868
|
+
} catch {
|
|
5869
|
+
}
|
|
5870
|
+
setItems((prev) => {
|
|
5871
|
+
const next = prev.map(
|
|
5872
|
+
(item) => item.id === id ? {
|
|
5873
|
+
...item,
|
|
5874
|
+
progress: 100,
|
|
5875
|
+
status: "succeeded",
|
|
5876
|
+
url: finalUrl,
|
|
5877
|
+
key: finalKey
|
|
5878
|
+
} : item
|
|
5879
|
+
);
|
|
5880
|
+
setTimeout(() => notifyChange(next), 0);
|
|
5881
|
+
return next;
|
|
5882
|
+
});
|
|
5883
|
+
} else {
|
|
5884
|
+
setItems(
|
|
5885
|
+
(prev) => prev.map(
|
|
5886
|
+
(item) => item.id === id ? {
|
|
5887
|
+
...item,
|
|
5888
|
+
status: "error",
|
|
5889
|
+
error: `Upload failed (Status ${xhr.status})`
|
|
5890
|
+
} : item
|
|
5891
|
+
)
|
|
5892
|
+
);
|
|
5893
|
+
}
|
|
5894
|
+
};
|
|
5895
|
+
xhr.onerror = () => {
|
|
5896
|
+
activeXhrsRef.current.delete(id);
|
|
5897
|
+
setItems(
|
|
5898
|
+
(prev) => prev.map(
|
|
5899
|
+
(item) => item.id === id ? { ...item, status: "error", error: "Network error during upload" } : item
|
|
5900
|
+
)
|
|
5901
|
+
);
|
|
5902
|
+
};
|
|
5903
|
+
if (presignData.fields && typeof presignData.fields === "object") {
|
|
5904
|
+
const formData = new FormData();
|
|
5905
|
+
Object.entries(presignData.fields).forEach(([k, v]) => {
|
|
5906
|
+
formData.append(k, String(v));
|
|
5907
|
+
});
|
|
5908
|
+
formData.append("file", file);
|
|
5909
|
+
xhr.open("POST", uploadUrl);
|
|
5910
|
+
xhr.send(formData);
|
|
5911
|
+
} else {
|
|
5912
|
+
xhr.open("PUT", uploadUrl);
|
|
5913
|
+
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
5914
|
+
if (presignData.headers && typeof presignData.headers === "object") {
|
|
5915
|
+
Object.entries(presignData.headers).forEach(([k, v]) => {
|
|
5916
|
+
xhr.setRequestHeader(k, String(v));
|
|
5917
|
+
});
|
|
5918
|
+
}
|
|
5919
|
+
xhr.send(file);
|
|
5920
|
+
}
|
|
5921
|
+
} catch (err) {
|
|
5922
|
+
setItems(
|
|
5923
|
+
(prev) => prev.map(
|
|
5924
|
+
(item) => item.id === id ? {
|
|
5925
|
+
...item,
|
|
5926
|
+
status: "error",
|
|
5927
|
+
error: err.message || "Failed to initialize upload"
|
|
5928
|
+
} : item
|
|
5929
|
+
)
|
|
5930
|
+
);
|
|
5931
|
+
}
|
|
5932
|
+
},
|
|
5933
|
+
[config, customProps]
|
|
5934
|
+
);
|
|
5935
|
+
const handleFilesAdded = React.useCallback(
|
|
5936
|
+
(files) => {
|
|
5937
|
+
setGeneralError(null);
|
|
5938
|
+
const incoming = Array.from(files);
|
|
5939
|
+
if (incoming.length === 0) return;
|
|
5940
|
+
const currentCount = items.filter((i) => i.status !== "error").length;
|
|
5941
|
+
if (currentCount + incoming.length > maxFiles) {
|
|
5942
|
+
setGeneralError(`You can upload a maximum of ${maxFiles} file${maxFiles === 1 ? "" : "s"}.`);
|
|
5943
|
+
return;
|
|
5944
|
+
}
|
|
5945
|
+
const validFiles = [];
|
|
5946
|
+
for (const file of incoming) {
|
|
5947
|
+
if (!isFileAccepted(file, acceptList)) {
|
|
5948
|
+
setGeneralError(`File "${file.name}" has an unsupported format.`);
|
|
5949
|
+
return;
|
|
5950
|
+
}
|
|
5951
|
+
if (file.size > maxSizeBytes) {
|
|
5952
|
+
setGeneralError(
|
|
5953
|
+
`File "${file.name}" exceeds the maximum size limit of ${maxSizeMb} MB.`
|
|
5954
|
+
);
|
|
5955
|
+
return;
|
|
5956
|
+
}
|
|
5957
|
+
const newId = `file-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
5958
|
+
validFiles.push({
|
|
5959
|
+
id: newId,
|
|
5960
|
+
file,
|
|
5961
|
+
name: file.name,
|
|
5962
|
+
size: file.size,
|
|
5963
|
+
type: file.type,
|
|
5964
|
+
progress: 0,
|
|
5965
|
+
status: "uploading"
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5968
|
+
setItems((prev) => {
|
|
5969
|
+
const next = maxFiles === 1 ? validFiles : [...prev, ...validFiles];
|
|
5970
|
+
return next;
|
|
5971
|
+
});
|
|
5972
|
+
validFiles.forEach((f) => uploadSingleFile(f));
|
|
5973
|
+
},
|
|
5974
|
+
[acceptList, items, maxFiles, maxSizeBytes, maxSizeMb, uploadSingleFile]
|
|
5975
|
+
);
|
|
5976
|
+
const removeItem = React.useCallback(
|
|
5977
|
+
(id) => {
|
|
5978
|
+
const activeXhr = activeXhrsRef.current.get(id);
|
|
5979
|
+
if (activeXhr) {
|
|
5980
|
+
activeXhr.abort();
|
|
5981
|
+
activeXhrsRef.current.delete(id);
|
|
5982
|
+
}
|
|
5983
|
+
setItems((prev) => {
|
|
5984
|
+
const next = prev.filter((item) => item.id !== id);
|
|
5985
|
+
setTimeout(() => notifyChange(next), 0);
|
|
5986
|
+
return next;
|
|
5987
|
+
});
|
|
5988
|
+
},
|
|
5989
|
+
[notifyChange]
|
|
5990
|
+
);
|
|
5991
|
+
const retryItem = React.useCallback(
|
|
5992
|
+
(id) => {
|
|
5993
|
+
const target = items.find((item) => item.id === id);
|
|
5994
|
+
if (!target || !target.file) return;
|
|
5995
|
+
setItems(
|
|
5996
|
+
(prev) => prev.map(
|
|
5997
|
+
(item) => item.id === id ? { ...item, progress: 0, status: "uploading", error: void 0 } : item
|
|
5998
|
+
)
|
|
5999
|
+
);
|
|
6000
|
+
uploadSingleFile({ ...target, progress: 0, status: "uploading", error: void 0 });
|
|
6001
|
+
},
|
|
6002
|
+
[items, uploadSingleFile]
|
|
6003
|
+
);
|
|
6004
|
+
const canAddMore = items.length < maxFiles && !disabled;
|
|
6005
|
+
const combinedError = error || generalError ? [
|
|
6006
|
+
...Array.isArray(error) ? error : error ? [String(error)] : [],
|
|
6007
|
+
...generalError ? [generalError] : []
|
|
6008
|
+
] : void 0;
|
|
6009
|
+
const c = props.theme?.colors;
|
|
6010
|
+
const inputRadius = props.theme?.shape?.inputRadius || "8px";
|
|
6011
|
+
const dropzoneStyle = {
|
|
6012
|
+
borderRadius: inputRadius,
|
|
6013
|
+
...c?.border && !isDragOver ? { borderColor: c.border } : {},
|
|
6014
|
+
...c?.surface ? { backgroundColor: isDragOver ? `${c.primary || "#3b82f6"}15` : c.surface } : {},
|
|
6015
|
+
...c?.text ? { color: c.text } : {}
|
|
6016
|
+
};
|
|
6017
|
+
const cardStyle = {
|
|
6018
|
+
borderRadius: inputRadius,
|
|
6019
|
+
...c?.border ? { borderColor: c.border } : {},
|
|
6020
|
+
...c?.surface ? { backgroundColor: c.surface } : {},
|
|
6021
|
+
...c?.text ? { color: c.text } : {}
|
|
6022
|
+
};
|
|
6023
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6024
|
+
fieldcraftReact.FieldWrapper,
|
|
6025
|
+
{
|
|
6026
|
+
field,
|
|
6027
|
+
error: combinedError,
|
|
6028
|
+
touched: touched || Boolean(generalError),
|
|
6029
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 font-sans", children: [
|
|
6030
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6031
|
+
"input",
|
|
6032
|
+
{
|
|
6033
|
+
ref: fileInputRef,
|
|
6034
|
+
type: "file",
|
|
6035
|
+
multiple: maxFiles > 1,
|
|
6036
|
+
accept: acceptList?.join(","),
|
|
6037
|
+
className: "hidden",
|
|
6038
|
+
disabled,
|
|
6039
|
+
onChange: (e) => {
|
|
6040
|
+
if (e.target.files) {
|
|
6041
|
+
handleFilesAdded(e.target.files);
|
|
6042
|
+
e.target.value = "";
|
|
6043
|
+
}
|
|
6044
|
+
},
|
|
6045
|
+
onBlur
|
|
6046
|
+
}
|
|
6047
|
+
),
|
|
6048
|
+
canAddMore && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6049
|
+
"div",
|
|
6050
|
+
{
|
|
6051
|
+
style: dropzoneStyle,
|
|
6052
|
+
onDragOver: (e) => {
|
|
6053
|
+
e.preventDefault();
|
|
6054
|
+
if (!disabled) setIsDragOver(true);
|
|
6055
|
+
},
|
|
6056
|
+
onDragLeave: () => setIsDragOver(false),
|
|
6057
|
+
onDrop: (e) => {
|
|
6058
|
+
e.preventDefault();
|
|
6059
|
+
setIsDragOver(false);
|
|
6060
|
+
if (!disabled && e.dataTransfer.files) {
|
|
6061
|
+
handleFilesAdded(e.dataTransfer.files);
|
|
6062
|
+
}
|
|
6063
|
+
},
|
|
6064
|
+
onClick: () => {
|
|
6065
|
+
if (!disabled) fileInputRef.current?.click();
|
|
6066
|
+
},
|
|
6067
|
+
className: `
|
|
6068
|
+
relative flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-xl cursor-pointer transition-all duration-200
|
|
6069
|
+
${isDragOver ? "border-primary bg-primary/5 scale-[0.99]" : "border-muted-foreground/25 hover:border-primary/60 hover:bg-muted/30 bg-muted/10"}
|
|
6070
|
+
${disabled ? "opacity-50 cursor-not-allowed" : ""}
|
|
6071
|
+
`,
|
|
6072
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center text-center space-y-2", children: [
|
|
6073
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-3 bg-primary/10 text-primary rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6074
|
+
"svg",
|
|
6075
|
+
{
|
|
6076
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
6077
|
+
className: "w-6 h-6",
|
|
6078
|
+
fill: "none",
|
|
6079
|
+
viewBox: "0 0 24 24",
|
|
6080
|
+
stroke: "currentColor",
|
|
6081
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6082
|
+
"path",
|
|
6083
|
+
{
|
|
6084
|
+
strokeLinecap: "round",
|
|
6085
|
+
strokeLinejoin: "round",
|
|
6086
|
+
strokeWidth: 2,
|
|
6087
|
+
d: "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
|
6088
|
+
}
|
|
6089
|
+
)
|
|
6090
|
+
}
|
|
6091
|
+
) }),
|
|
6092
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm", children: [
|
|
6093
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: "Click to upload" }),
|
|
6094
|
+
" ",
|
|
6095
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "or drag and drop" })
|
|
6096
|
+
] }),
|
|
6097
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
6098
|
+
acceptList && acceptList.length > 0 ? `Supported: ${acceptList.join(", ")}` : "All file types supported",
|
|
6099
|
+
" ",
|
|
6100
|
+
"(Max ",
|
|
6101
|
+
maxSizeMb,
|
|
6102
|
+
" MB)"
|
|
6103
|
+
] })
|
|
6104
|
+
] })
|
|
6105
|
+
}
|
|
6106
|
+
),
|
|
6107
|
+
items.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: items.map((item) => {
|
|
6108
|
+
const isImage = item.type.startsWith("image/") && item.url;
|
|
6109
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6110
|
+
"div",
|
|
6111
|
+
{
|
|
6112
|
+
style: cardStyle,
|
|
6113
|
+
className: "flex items-center justify-between p-3 bg-card border rounded-lg shadow-sm text-sm",
|
|
6114
|
+
children: [
|
|
6115
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-3 min-w-0 flex-1 mr-3", children: [
|
|
6116
|
+
isImage ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
6117
|
+
"img",
|
|
6118
|
+
{
|
|
6119
|
+
src: item.url,
|
|
6120
|
+
alt: item.name,
|
|
6121
|
+
className: "w-10 h-10 rounded object-cover shrink-0 border"
|
|
6122
|
+
}
|
|
6123
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-10 h-10 rounded bg-muted flex items-center justify-center shrink-0 text-muted-foreground font-mono text-xs uppercase font-bold", children: item.name.split(".").pop()?.slice(0, 4) || "FILE" }),
|
|
6124
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
6125
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
6126
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-foreground truncate", children: item.name }),
|
|
6127
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground shrink-0 ml-2", children: formatBytes(item.size) })
|
|
6128
|
+
] }),
|
|
6129
|
+
item.status === "uploading" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1.5 w-full bg-muted rounded-full h-1.5 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6130
|
+
"div",
|
|
6131
|
+
{
|
|
6132
|
+
className: "bg-primary h-1.5 rounded-full transition-all duration-150",
|
|
6133
|
+
style: { width: `${item.progress}%` }
|
|
6134
|
+
}
|
|
6135
|
+
) }),
|
|
6136
|
+
item.status === "error" && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive mt-0.5", children: item.error || "Upload failed" }),
|
|
6137
|
+
item.status === "succeeded" && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-emerald-600 dark:text-emerald-400 mt-0.5 flex items-center gap-1", children: [
|
|
6138
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6139
|
+
"path",
|
|
6140
|
+
{
|
|
6141
|
+
fillRule: "evenodd",
|
|
6142
|
+
d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
|
|
6143
|
+
clipRule: "evenodd"
|
|
6144
|
+
}
|
|
6145
|
+
) }),
|
|
6146
|
+
"Uploaded"
|
|
6147
|
+
] })
|
|
6148
|
+
] })
|
|
6149
|
+
] }),
|
|
6150
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-1.5 shrink-0", children: [
|
|
6151
|
+
item.status === "error" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6152
|
+
"button",
|
|
6153
|
+
{
|
|
6154
|
+
type: "button",
|
|
6155
|
+
onClick: () => retryItem(item.id),
|
|
6156
|
+
className: "p-1 text-xs text-primary hover:underline",
|
|
6157
|
+
title: "Retry upload",
|
|
6158
|
+
children: "Retry"
|
|
6159
|
+
}
|
|
6160
|
+
),
|
|
6161
|
+
item.status === "succeeded" && item.url && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6162
|
+
"a",
|
|
6163
|
+
{
|
|
6164
|
+
href: item.url,
|
|
6165
|
+
target: "_blank",
|
|
6166
|
+
rel: "noreferrer",
|
|
6167
|
+
className: "p-1.5 text-muted-foreground hover:text-foreground rounded transition-colors",
|
|
6168
|
+
title: "View file",
|
|
6169
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6170
|
+
"path",
|
|
6171
|
+
{
|
|
6172
|
+
strokeLinecap: "round",
|
|
6173
|
+
strokeLinejoin: "round",
|
|
6174
|
+
strokeWidth: 2,
|
|
6175
|
+
d: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
6176
|
+
}
|
|
6177
|
+
) })
|
|
6178
|
+
}
|
|
6179
|
+
),
|
|
6180
|
+
!disabled && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6181
|
+
"button",
|
|
6182
|
+
{
|
|
6183
|
+
type: "button",
|
|
6184
|
+
onClick: () => removeItem(item.id),
|
|
6185
|
+
className: "p-1.5 text-muted-foreground hover:text-destructive rounded transition-colors",
|
|
6186
|
+
title: "Remove file",
|
|
6187
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6188
|
+
"path",
|
|
6189
|
+
{
|
|
6190
|
+
strokeLinecap: "round",
|
|
6191
|
+
strokeLinejoin: "round",
|
|
6192
|
+
strokeWidth: 2,
|
|
6193
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
6194
|
+
}
|
|
6195
|
+
) })
|
|
6196
|
+
}
|
|
6197
|
+
)
|
|
6198
|
+
] })
|
|
6199
|
+
]
|
|
6200
|
+
},
|
|
6201
|
+
item.id
|
|
6202
|
+
);
|
|
6203
|
+
}) })
|
|
6204
|
+
] })
|
|
6205
|
+
}
|
|
6206
|
+
);
|
|
6207
|
+
}
|
|
6208
|
+
var ProFileUploadField_default = ProFileUploadField;
|
|
6209
|
+
function extractAttendeeInfo(fieldValues) {
|
|
6210
|
+
if (!fieldValues) return {};
|
|
6211
|
+
let name;
|
|
6212
|
+
let email;
|
|
6213
|
+
for (const [key, val] of Object.entries(fieldValues)) {
|
|
6214
|
+
const k = key.toLowerCase();
|
|
6215
|
+
if (!name && (k.includes("name") || k === "fullname" || k === "first_name")) {
|
|
6216
|
+
if (typeof val === "string" && val.trim()) {
|
|
6217
|
+
name = val.trim();
|
|
6218
|
+
} else if (val && typeof val === "object" && "first" in val) {
|
|
6219
|
+
const obj = val;
|
|
6220
|
+
name = [obj.first, obj.middle, obj.last].filter(Boolean).join(" ").trim();
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
if (!email && (k.includes("email") || k.includes("mail"))) {
|
|
6224
|
+
if (typeof val === "string" && val.includes("@")) {
|
|
6225
|
+
email = val.trim();
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
return { name, email };
|
|
6230
|
+
}
|
|
6231
|
+
function formatTime12h(timeStr) {
|
|
6232
|
+
const [hStr, mStr] = timeStr.split(":");
|
|
6233
|
+
const h = parseInt(hStr, 10);
|
|
6234
|
+
if (isNaN(h)) return timeStr;
|
|
6235
|
+
const ampm = h >= 12 ? "PM" : "AM";
|
|
6236
|
+
const h12 = h % 12 || 12;
|
|
6237
|
+
return `${h12}:${mStr || "00"} ${ampm}`;
|
|
6238
|
+
}
|
|
6239
|
+
function ProAppointmentField(props) {
|
|
6240
|
+
const {
|
|
6241
|
+
value,
|
|
6242
|
+
onChange,
|
|
6243
|
+
onBlur,
|
|
6244
|
+
error,
|
|
6245
|
+
touched,
|
|
6246
|
+
disabled = false,
|
|
6247
|
+
fieldValues,
|
|
6248
|
+
customProps
|
|
6249
|
+
} = props;
|
|
6250
|
+
const onNext = customProps?.onNext || props.onNext;
|
|
6251
|
+
const field = props.field || props.question;
|
|
6252
|
+
const config = field?.config ?? {};
|
|
6253
|
+
const provider = React.useMemo(() => {
|
|
6254
|
+
if (config.embedUrl) {
|
|
6255
|
+
if (config.embedProvider === "calendly" || config.embedUrl.includes("calendly.com")) {
|
|
6256
|
+
return "calendly";
|
|
6257
|
+
}
|
|
6258
|
+
return "cal_com";
|
|
6259
|
+
}
|
|
6260
|
+
return "slots";
|
|
6261
|
+
}, [config.embedUrl, config.embedProvider]);
|
|
6262
|
+
const booking = value ?? null;
|
|
6263
|
+
const isConfirmed = booking?.status === "confirmed";
|
|
6264
|
+
const resolvedTimezone = React.useMemo(() => {
|
|
6265
|
+
if (config.timezoneField && fieldValues?.[config.timezoneField]) {
|
|
6266
|
+
const dynamicVal = String(fieldValues[config.timezoneField]);
|
|
6267
|
+
if (dynamicVal.trim()) return dynamicVal.trim();
|
|
6268
|
+
}
|
|
6269
|
+
if (config.timezone) return config.timezone;
|
|
6270
|
+
try {
|
|
6271
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
6272
|
+
} catch {
|
|
6273
|
+
return "UTC";
|
|
6274
|
+
}
|
|
6275
|
+
}, [config.timezone, config.timezoneField, fieldValues]);
|
|
6276
|
+
const [selectedDate, setSelectedDate] = React.useState(() => {
|
|
6277
|
+
if (booking?.date) return booking.date;
|
|
6278
|
+
if (config.slots && config.slots.length > 0) return config.slots[0].date;
|
|
6279
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
6280
|
+
});
|
|
6281
|
+
const [selectedTime, setSelectedTime] = React.useState(() => booking?.time || null);
|
|
6282
|
+
const [remoteSlots, setRemoteSlots] = React.useState(config.slots || []);
|
|
6283
|
+
const [isLoadingSlots, setIsLoadingSlots] = React.useState(false);
|
|
6284
|
+
const [slotsFetchError, setSlotsFetchError] = React.useState(null);
|
|
6285
|
+
React.useEffect(() => {
|
|
6286
|
+
if (provider !== "slots" || !config.slotsUrl) return;
|
|
6287
|
+
let isMounted = true;
|
|
6288
|
+
setIsLoadingSlots(true);
|
|
6289
|
+
setSlotsFetchError(null);
|
|
6290
|
+
fetch(config.slotsUrl).then((res) => {
|
|
6291
|
+
if (!res.ok) throw new Error(`Slots API returned status ${res.status}`);
|
|
6292
|
+
return res.json();
|
|
6293
|
+
}).then((data) => {
|
|
6294
|
+
if (isMounted) {
|
|
6295
|
+
const slotsList = Array.isArray(data) ? data : data.slots || [];
|
|
6296
|
+
setRemoteSlots(slotsList);
|
|
6297
|
+
if (slotsList.length > 0 && !selectedDate) {
|
|
6298
|
+
setSelectedDate(slotsList[0].date);
|
|
6299
|
+
}
|
|
6300
|
+
setIsLoadingSlots(false);
|
|
6301
|
+
}
|
|
6302
|
+
}).catch((err) => {
|
|
6303
|
+
if (isMounted) {
|
|
6304
|
+
setSlotsFetchError(err.message || "Failed to load available slots");
|
|
6305
|
+
setIsLoadingSlots(false);
|
|
6306
|
+
}
|
|
6307
|
+
});
|
|
6308
|
+
return () => {
|
|
6309
|
+
isMounted = false;
|
|
6310
|
+
};
|
|
6311
|
+
}, [config.slotsUrl, provider, selectedDate]);
|
|
6312
|
+
const embedSrc = React.useMemo(() => {
|
|
6313
|
+
if (!config.embedUrl) return "";
|
|
6314
|
+
try {
|
|
6315
|
+
const url = new URL(config.embedUrl);
|
|
6316
|
+
const { name, email } = extractAttendeeInfo(fieldValues);
|
|
6317
|
+
if (name && !url.searchParams.has("name")) url.searchParams.set("name", name);
|
|
6318
|
+
if (email && !url.searchParams.has("email")) url.searchParams.set("email", email);
|
|
6319
|
+
if (resolvedTimezone && !url.searchParams.has("timezone")) {
|
|
6320
|
+
url.searchParams.set("timezone", resolvedTimezone);
|
|
6321
|
+
}
|
|
6322
|
+
if (provider === "cal_com") {
|
|
6323
|
+
if (!url.searchParams.has("embed")) url.searchParams.set("embed", "true");
|
|
6324
|
+
if (!url.searchParams.has("layout")) url.searchParams.set("layout", "month_view");
|
|
6325
|
+
} else if (provider === "calendly") {
|
|
6326
|
+
if (!url.searchParams.has("embed_type")) url.searchParams.set("embed_type", "Inline");
|
|
6327
|
+
}
|
|
6328
|
+
return url.toString();
|
|
6329
|
+
} catch {
|
|
6330
|
+
return config.embedUrl;
|
|
6331
|
+
}
|
|
6332
|
+
}, [config.embedUrl, fieldValues, provider, resolvedTimezone]);
|
|
6333
|
+
React.useEffect(() => {
|
|
6334
|
+
const handleMessage = (event) => {
|
|
6335
|
+
let data = event.data;
|
|
6336
|
+
if (!data) return;
|
|
6337
|
+
if (typeof data === "string") {
|
|
6338
|
+
try {
|
|
6339
|
+
data = JSON.parse(data);
|
|
6340
|
+
} catch {
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
if (typeof data !== "object" || data === null) return;
|
|
6344
|
+
if (data.type === "cal:bookingSuccessful" || data.origin === "CAL" && data.action === "bookingSuccessful" || data.event === "cal:bookingSuccessful") {
|
|
6345
|
+
const payload = data.data || data.payload || {};
|
|
6346
|
+
const newBooking = {
|
|
6347
|
+
provider: "cal_com",
|
|
6348
|
+
status: "confirmed",
|
|
6349
|
+
bookingId: String(payload.bookingId || payload.id || `cal-${Date.now()}`),
|
|
6350
|
+
date: payload.date || payload.startTime?.split("T")[0],
|
|
6351
|
+
time: payload.time || payload.startTime?.split("T")[1]?.slice(0, 5),
|
|
6352
|
+
duration: payload.duration || config.duration || 30,
|
|
6353
|
+
timezone: payload.timezone || resolvedTimezone,
|
|
6354
|
+
eventTitle: payload.eventTitle || payload.title,
|
|
6355
|
+
attendeeName: payload.name || payload.attendeeName,
|
|
6356
|
+
attendeeEmail: payload.email || payload.attendeeEmail,
|
|
6357
|
+
confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6358
|
+
};
|
|
6359
|
+
onChange(newBooking);
|
|
6360
|
+
if (config.autoAdvance !== false && typeof onNext === "function") {
|
|
6361
|
+
setTimeout(() => onNext(), 400);
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
if (data.event === "calendly.event_scheduled" || data.action === "calendly.event_scheduled" || data.type === "calendly.event_scheduled" || data.event === "event_scheduled") {
|
|
6365
|
+
const payload = data.payload || data.data || {};
|
|
6366
|
+
const newBooking = {
|
|
6367
|
+
provider: "calendly",
|
|
6368
|
+
status: "confirmed",
|
|
6369
|
+
eventUri: payload.event?.uri,
|
|
6370
|
+
inviteeUri: payload.invitee?.uri,
|
|
6371
|
+
bookingId: payload.event?.uri ? String(payload.event.uri).split("/").pop() : `cal-${Date.now()}`,
|
|
6372
|
+
duration: config.duration || 30,
|
|
6373
|
+
timezone: resolvedTimezone,
|
|
6374
|
+
confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6375
|
+
};
|
|
6376
|
+
onChange(newBooking);
|
|
6377
|
+
if (config.autoAdvance !== false && typeof onNext === "function") {
|
|
6378
|
+
setTimeout(() => onNext(), 400);
|
|
6379
|
+
}
|
|
6380
|
+
}
|
|
6381
|
+
};
|
|
6382
|
+
window.addEventListener("message", handleMessage);
|
|
6383
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
6384
|
+
}, [config.autoAdvance, config.duration, onChange, onNext, resolvedTimezone]);
|
|
6385
|
+
const handleSlotSelect = React.useCallback(
|
|
6386
|
+
(time) => {
|
|
6387
|
+
setSelectedTime(time);
|
|
6388
|
+
const newBooking = {
|
|
6389
|
+
provider: "slots",
|
|
6390
|
+
status: "confirmed",
|
|
6391
|
+
date: selectedDate,
|
|
6392
|
+
time,
|
|
6393
|
+
duration: config.duration || 30,
|
|
6394
|
+
timezone: resolvedTimezone,
|
|
6395
|
+
confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6396
|
+
};
|
|
6397
|
+
onChange(newBooking);
|
|
6398
|
+
},
|
|
6399
|
+
[config.duration, onChange, resolvedTimezone, selectedDate]
|
|
6400
|
+
);
|
|
6401
|
+
const handleResetBooking = React.useCallback(() => {
|
|
6402
|
+
onChange(null);
|
|
6403
|
+
setSelectedTime(null);
|
|
6404
|
+
}, [onChange]);
|
|
6405
|
+
const availableTimes = React.useMemo(() => {
|
|
6406
|
+
const dayMatch = remoteSlots.find((s) => s.date === selectedDate);
|
|
6407
|
+
if (dayMatch && Array.isArray(dayMatch.times) && dayMatch.times.length > 0) {
|
|
6408
|
+
return dayMatch.times;
|
|
6409
|
+
}
|
|
6410
|
+
return [
|
|
6411
|
+
"09:00",
|
|
6412
|
+
"09:30",
|
|
6413
|
+
"10:00",
|
|
6414
|
+
"10:30",
|
|
6415
|
+
"11:00",
|
|
6416
|
+
"11:30",
|
|
6417
|
+
"13:00",
|
|
6418
|
+
"13:30",
|
|
6419
|
+
"14:00",
|
|
6420
|
+
"14:30",
|
|
6421
|
+
"15:00",
|
|
6422
|
+
"15:30",
|
|
6423
|
+
"16:00"
|
|
6424
|
+
];
|
|
6425
|
+
}, [remoteSlots, selectedDate]);
|
|
6426
|
+
const combinedError = error || slotsFetchError ? [
|
|
6427
|
+
...Array.isArray(error) ? error : error ? [String(error)] : [],
|
|
6428
|
+
...slotsFetchError ? [slotsFetchError] : []
|
|
6429
|
+
] : void 0;
|
|
6430
|
+
const c = props.theme?.colors;
|
|
6431
|
+
const inputRadius = props.theme?.shape?.inputRadius || "8px";
|
|
6432
|
+
const cardStyle = {
|
|
6433
|
+
borderRadius: inputRadius,
|
|
6434
|
+
...c?.border ? { borderColor: c.border } : {},
|
|
6435
|
+
...c?.surface ? { backgroundColor: c.surface } : {},
|
|
6436
|
+
...c?.text ? { color: c.text } : {}
|
|
6437
|
+
};
|
|
6438
|
+
const activeSlotStyle = {
|
|
6439
|
+
borderRadius: inputRadius,
|
|
6440
|
+
...c?.primary ? { backgroundColor: c.primary, borderColor: c.primary } : {},
|
|
6441
|
+
...c?.primaryForeground ? { color: c.primaryForeground } : {}
|
|
6442
|
+
};
|
|
6443
|
+
const inactiveSlotStyle = {
|
|
6444
|
+
borderRadius: inputRadius,
|
|
6445
|
+
...c?.border ? { borderColor: c.border } : {},
|
|
6446
|
+
...c?.surface ? { backgroundColor: c.surface } : {},
|
|
6447
|
+
...c?.text ? { color: c.text } : {}
|
|
6448
|
+
};
|
|
6449
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error: combinedError, touched: touched || Boolean(slotsFetchError), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 font-sans", children: [
|
|
6450
|
+
isConfirmed && booking && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6451
|
+
"div",
|
|
6452
|
+
{
|
|
6453
|
+
style: cardStyle,
|
|
6454
|
+
className: "p-5 border border-emerald-500/30 bg-emerald-500/5 rounded-xl text-foreground space-y-3",
|
|
6455
|
+
children: [
|
|
6456
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
6457
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-2.5", children: [
|
|
6458
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6459
|
+
"path",
|
|
6460
|
+
{
|
|
6461
|
+
fillRule: "evenodd",
|
|
6462
|
+
d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
|
|
6463
|
+
clipRule: "evenodd"
|
|
6464
|
+
}
|
|
6465
|
+
) }) }),
|
|
6466
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
6467
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold text-foreground", children: "Appointment Confirmed" }),
|
|
6468
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground capitalize", children: [
|
|
6469
|
+
"via ",
|
|
6470
|
+
booking.provider.replace("_", ".")
|
|
6471
|
+
] })
|
|
6472
|
+
] })
|
|
6473
|
+
] }),
|
|
6474
|
+
!disabled && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6475
|
+
"button",
|
|
6476
|
+
{
|
|
6477
|
+
type: "button",
|
|
6478
|
+
onClick: handleResetBooking,
|
|
6479
|
+
className: "text-xs text-muted-foreground hover:text-foreground underline transition-colors",
|
|
6480
|
+
children: "Reschedule"
|
|
6481
|
+
}
|
|
6482
|
+
)
|
|
6483
|
+
] }),
|
|
6484
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2 text-xs pt-1 border-t border-emerald-500/20", children: [
|
|
6485
|
+
booking.date && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
6486
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Date:" }),
|
|
6487
|
+
" ",
|
|
6488
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: booking.date })
|
|
6489
|
+
] }),
|
|
6490
|
+
booking.time && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
6491
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Time:" }),
|
|
6492
|
+
" ",
|
|
6493
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatTime12h(booking.time) })
|
|
6494
|
+
] }),
|
|
6495
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
6496
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Timezone:" }),
|
|
6497
|
+
" ",
|
|
6498
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: booking.timezone || resolvedTimezone })
|
|
6499
|
+
] }),
|
|
6500
|
+
booking.duration && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
6501
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Duration:" }),
|
|
6502
|
+
" ",
|
|
6503
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-foreground", children: [
|
|
6504
|
+
booking.duration,
|
|
6505
|
+
" mins"
|
|
6506
|
+
] })
|
|
6507
|
+
] })
|
|
6508
|
+
] }),
|
|
6509
|
+
typeof onNext === "function" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-2 border-t border-emerald-500/20 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6510
|
+
"button",
|
|
6511
|
+
{
|
|
6512
|
+
type: "button",
|
|
6513
|
+
onClick: onNext,
|
|
6514
|
+
className: "px-3.5 py-1.5 text-xs font-semibold rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition-colors flex items-center gap-1",
|
|
6515
|
+
children: [
|
|
6516
|
+
"Continue",
|
|
6517
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3.5 h-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })
|
|
6518
|
+
]
|
|
6519
|
+
}
|
|
6520
|
+
) })
|
|
6521
|
+
]
|
|
6522
|
+
}
|
|
6523
|
+
),
|
|
6524
|
+
!isConfirmed && (provider === "cal_com" || provider === "calendly") && embedSrc && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
6525
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6526
|
+
"div",
|
|
6527
|
+
{
|
|
6528
|
+
style: cardStyle,
|
|
6529
|
+
className: "w-full border rounded-xl overflow-hidden shadow-sm bg-card",
|
|
6530
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6531
|
+
"iframe",
|
|
6532
|
+
{
|
|
6533
|
+
src: embedSrc,
|
|
6534
|
+
title: "Schedule Appointment",
|
|
6535
|
+
className: "w-full h-[580px] border-0",
|
|
6536
|
+
allow: "camera; microphone; autoplay; fullscreen; payment",
|
|
6537
|
+
loading: "lazy"
|
|
6538
|
+
}
|
|
6539
|
+
)
|
|
6540
|
+
}
|
|
6541
|
+
),
|
|
6542
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-2 text-xs text-muted-foreground px-1", children: [
|
|
6543
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Finished scheduling in the calendar above?" }),
|
|
6544
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6545
|
+
"button",
|
|
6546
|
+
{
|
|
6547
|
+
type: "button",
|
|
6548
|
+
onClick: () => {
|
|
6549
|
+
const manualBooking = {
|
|
6550
|
+
provider,
|
|
6551
|
+
status: "confirmed",
|
|
6552
|
+
bookingId: `manual-${Date.now()}`,
|
|
6553
|
+
timezone: resolvedTimezone,
|
|
6554
|
+
confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6555
|
+
};
|
|
6556
|
+
onChange(manualBooking);
|
|
6557
|
+
if (typeof onNext === "function") {
|
|
6558
|
+
setTimeout(() => onNext(), 300);
|
|
6559
|
+
}
|
|
6560
|
+
},
|
|
6561
|
+
className: "px-3 py-1.5 font-medium rounded-lg border border-border bg-background hover:bg-muted text-foreground transition-colors flex items-center justify-center gap-1 shadow-sm self-end sm:self-auto",
|
|
6562
|
+
children: "\u2713 I've Completed My Booking"
|
|
5238
6563
|
}
|
|
5239
6564
|
)
|
|
5240
|
-
]
|
|
5241
|
-
}
|
|
5242
|
-
) });
|
|
5243
|
-
}
|
|
5244
|
-
function DragOverlayContent({
|
|
5245
|
-
item,
|
|
5246
|
-
schema,
|
|
5247
|
-
questionTypes: types
|
|
5248
|
-
}) {
|
|
5249
|
-
if (item.type === "palette-item") {
|
|
5250
|
-
const typeInfo = types[item.questionType];
|
|
5251
|
-
if (!typeInfo) return null;
|
|
5252
|
-
const IconComponent = getIcon(typeInfo.icon);
|
|
5253
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2 rounded-md border border-primary bg-card text-foreground text-sm fcb-shadow-lg cursor-grabbing", children: [
|
|
5254
|
-
/* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 14, className: "shrink-0 text-primary", strokeWidth: 1.75 }),
|
|
5255
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: typeInfo.label })
|
|
5256
|
-
] });
|
|
5257
|
-
}
|
|
5258
|
-
if (item.type === "question") {
|
|
5259
|
-
const found = findQuestion(schema, item.sectionId, item.questionId);
|
|
5260
|
-
if (!found) return null;
|
|
5261
|
-
const typeInfo = types[found.question.type];
|
|
5262
|
-
const IconComponent = typeInfo ? getIcon(typeInfo.icon) : null;
|
|
5263
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
5264
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 mb-1", children: /* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Badge, { variant: "secondary", className: "gap-1.5 text-muted-foreground", children: [
|
|
5265
|
-
IconComponent && /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 11, strokeWidth: 1.75 }),
|
|
5266
|
-
typeInfo?.label ?? found.question.type
|
|
5267
|
-
] }) }),
|
|
5268
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: found.question.label })
|
|
5269
|
-
] });
|
|
5270
|
-
}
|
|
5271
|
-
if (item.type === "section") {
|
|
5272
|
-
const section = schema.sections.find((s) => s.id === item.sectionId);
|
|
5273
|
-
if (!section) return null;
|
|
5274
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
5275
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold", children: section.title }),
|
|
5276
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: [
|
|
5277
|
-
section.questions.length,
|
|
5278
|
-
" field",
|
|
5279
|
-
section.questions.length !== 1 ? "s" : ""
|
|
5280
6565
|
] })
|
|
5281
|
-
] })
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
6566
|
+
] }),
|
|
6567
|
+
!isConfirmed && provider === "slots" && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6568
|
+
"div",
|
|
6569
|
+
{
|
|
6570
|
+
style: cardStyle,
|
|
6571
|
+
className: "space-y-4 p-4 border rounded-xl bg-card shadow-sm",
|
|
6572
|
+
children: [
|
|
6573
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-3 border-b", children: [
|
|
6574
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
6575
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-semibold text-foreground block mb-1", children: "Select Date" }),
|
|
6576
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6577
|
+
"input",
|
|
6578
|
+
{
|
|
6579
|
+
type: "date",
|
|
6580
|
+
value: selectedDate,
|
|
6581
|
+
min: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
6582
|
+
disabled,
|
|
6583
|
+
onChange: (e) => setSelectedDate(e.target.value),
|
|
6584
|
+
onBlur,
|
|
6585
|
+
className: "px-3 py-1.5 text-sm border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"
|
|
6586
|
+
}
|
|
6587
|
+
)
|
|
6588
|
+
] }),
|
|
6589
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-1.5 self-start sm:self-auto", children: [
|
|
6590
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-muted-foreground/70", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" }) }),
|
|
6591
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
6592
|
+
"Timezone: ",
|
|
6593
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-foreground", children: resolvedTimezone })
|
|
6594
|
+
] })
|
|
6595
|
+
] })
|
|
6596
|
+
] }),
|
|
6597
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
6598
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-semibold text-foreground block", children: [
|
|
6599
|
+
"Available Times (",
|
|
6600
|
+
config.duration || 30,
|
|
6601
|
+
" mins)"
|
|
6602
|
+
] }),
|
|
6603
|
+
isLoadingSlots ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-8 text-center text-xs text-muted-foreground", children: "Loading available slots..." }) : availableTimes.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2", children: availableTimes.map((timeStr) => {
|
|
6604
|
+
const isSelected = selectedTime === timeStr;
|
|
6605
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6606
|
+
"button",
|
|
6607
|
+
{
|
|
6608
|
+
type: "button",
|
|
6609
|
+
style: isSelected ? activeSlotStyle : inactiveSlotStyle,
|
|
6610
|
+
disabled,
|
|
6611
|
+
onClick: () => handleSlotSelect(timeStr),
|
|
6612
|
+
className: `
|
|
6613
|
+
px-2.5 py-2 text-xs font-medium rounded-lg border transition-all duration-150 text-center
|
|
6614
|
+
${isSelected ? "bg-primary text-primary-foreground border-primary shadow-sm ring-2 ring-primary/20 scale-[1.02]" : "bg-background text-foreground border-border hover:border-primary/60 hover:bg-muted/50"}
|
|
6615
|
+
${disabled ? "opacity-50 cursor-not-allowed" : ""}
|
|
6616
|
+
`,
|
|
6617
|
+
children: formatTime12h(timeStr)
|
|
6618
|
+
},
|
|
6619
|
+
timeStr
|
|
6620
|
+
);
|
|
6621
|
+
}) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-6 text-center text-xs text-muted-foreground", children: "No slots available on this date. Please select another date." })
|
|
6622
|
+
] })
|
|
6623
|
+
]
|
|
6624
|
+
}
|
|
6625
|
+
)
|
|
6626
|
+
] }) });
|
|
5287
6627
|
}
|
|
5288
|
-
|
|
5289
|
-
// src/form-builder/components/FormBuilderGated.tsx
|
|
5290
|
-
var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
|
|
6628
|
+
var ProAppointmentField_default = ProAppointmentField;
|
|
5291
6629
|
function resolvePath(obj, path) {
|
|
5292
6630
|
let current = obj;
|
|
5293
6631
|
for (const key of path.split(".")) {
|
|
@@ -5384,13 +6722,16 @@ function ProPaymentField(props) {
|
|
|
5384
6722
|
onChange,
|
|
5385
6723
|
onBlur,
|
|
5386
6724
|
customProps,
|
|
5387
|
-
theme: formTheme
|
|
6725
|
+
theme: formTheme,
|
|
6726
|
+
fieldValues
|
|
5388
6727
|
} = props;
|
|
5389
6728
|
const config = field.config;
|
|
5390
6729
|
const current = value ?? { status: "pending" };
|
|
5391
6730
|
const provider = config?.provider ?? "stripe";
|
|
5392
6731
|
const publicKey = config?.publicKey;
|
|
5393
|
-
const
|
|
6732
|
+
const rawDynamicAmount = config?.amountField && fieldValues ? fieldValues[config.amountField] : void 0;
|
|
6733
|
+
const dynamicAmountCents = typeof rawDynamicAmount === "number" ? Math.round(rawDynamicAmount * 100) : typeof rawDynamicAmount === "string" && !isNaN(Number(rawDynamicAmount)) && rawDynamicAmount !== "" ? Math.round(Number(rawDynamicAmount) * 100) : void 0;
|
|
6734
|
+
const amount = customProps?.amount ?? dynamicAmountCents ?? config?.amount;
|
|
5394
6735
|
const currency = config?.currency ?? "USD";
|
|
5395
6736
|
const directSecret = customProps?.clientSecret;
|
|
5396
6737
|
const onCreateIntent = customProps?.onCreatePaymentIntent;
|
|
@@ -5399,6 +6740,15 @@ function ProPaymentField(props) {
|
|
|
5399
6740
|
const [clientSecret, setClientSecret] = React.useState(directSecret);
|
|
5400
6741
|
const [intentLoading, setIntentLoading] = React.useState(false);
|
|
5401
6742
|
const [intentError, setIntentError] = React.useState(null);
|
|
6743
|
+
const prevAmountRef = React.useRef(amount);
|
|
6744
|
+
React.useEffect(() => {
|
|
6745
|
+
if (prevAmountRef.current !== amount) {
|
|
6746
|
+
prevAmountRef.current = amount;
|
|
6747
|
+
if (!directSecret) {
|
|
6748
|
+
setClientSecret(void 0);
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
}, [amount, directSecret]);
|
|
5402
6752
|
const mode = directSecret ? "direct" : onCreateIntent ? "callback" : serverUrl ? "url" : "setup";
|
|
5403
6753
|
React.useEffect(() => {
|
|
5404
6754
|
if (directSecret) {
|
|
@@ -5465,7 +6815,7 @@ function ProPaymentField(props) {
|
|
|
5465
6815
|
const c = formTheme.colors;
|
|
5466
6816
|
const cardStyle = {
|
|
5467
6817
|
borderRadius: formTheme.shape?.inputRadius || "8px",
|
|
5468
|
-
border: `1px solid ${c?.border || "
|
|
6818
|
+
border: `1px solid ${c?.border || "var(--rule)"}`,
|
|
5469
6819
|
padding: "16px",
|
|
5470
6820
|
background: c?.surface || c?.background,
|
|
5471
6821
|
color: c?.text
|
|
@@ -5753,10 +7103,29 @@ function PayKitCheckout({
|
|
|
5753
7103
|
) });
|
|
5754
7104
|
}
|
|
5755
7105
|
var PRO_FIELD_OVERRIDES = {
|
|
5756
|
-
payment: ProPaymentField
|
|
7106
|
+
payment: ProPaymentField,
|
|
7107
|
+
file_upload: ProFileUploadField_default,
|
|
7108
|
+
appointment: ProAppointmentField_default
|
|
5757
7109
|
};
|
|
7110
|
+
|
|
7111
|
+
// src/response-viewer/constants.ts
|
|
7112
|
+
var DISPLAY_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
7113
|
+
"info-block",
|
|
7114
|
+
"info_block",
|
|
7115
|
+
"section-header",
|
|
7116
|
+
"page-break",
|
|
7117
|
+
"image",
|
|
7118
|
+
"divider",
|
|
7119
|
+
"spacer",
|
|
7120
|
+
"video",
|
|
7121
|
+
"rich-text",
|
|
7122
|
+
"welcome-screen",
|
|
7123
|
+
"thank-you-screen",
|
|
7124
|
+
"hidden",
|
|
7125
|
+
"calculated"
|
|
7126
|
+
]);
|
|
5758
7127
|
function formatDuration(ms) {
|
|
5759
|
-
if (ms == null) return "\u2014";
|
|
7128
|
+
if (ms == null || ms < 0) return "\u2014";
|
|
5760
7129
|
const totalSeconds = Math.round(ms / 1e3);
|
|
5761
7130
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
5762
7131
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -5833,11 +7202,7 @@ function ResponseTable({
|
|
|
5833
7202
|
) }),
|
|
5834
7203
|
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
|
|
5835
7204
|
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: formatDuration(response.completionTimeMs) }),
|
|
5836
|
-
/* @__PURE__ */ jsxRuntime.
|
|
5837
|
-
answered,
|
|
5838
|
-
"/",
|
|
5839
|
-
totalFields
|
|
5840
|
-
] }),
|
|
7205
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: totalFields > 0 ? `${answered}/${totalFields}` : "\u2014" }),
|
|
5841
7206
|
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground whitespace-nowrap", children: response.totalScore ?? "\u2014" })
|
|
5842
7207
|
]
|
|
5843
7208
|
},
|
|
@@ -5855,21 +7220,6 @@ function ResponseTable({
|
|
|
5855
7220
|
] })
|
|
5856
7221
|
] }) });
|
|
5857
7222
|
}
|
|
5858
|
-
var DISPLAY_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
5859
|
-
"info-block",
|
|
5860
|
-
"info_block",
|
|
5861
|
-
"section-header",
|
|
5862
|
-
"page-break",
|
|
5863
|
-
"image",
|
|
5864
|
-
"divider",
|
|
5865
|
-
"spacer",
|
|
5866
|
-
"video",
|
|
5867
|
-
"rich-text",
|
|
5868
|
-
"welcome-screen",
|
|
5869
|
-
"thank-you-screen",
|
|
5870
|
-
"hidden",
|
|
5871
|
-
"calculated"
|
|
5872
|
-
]);
|
|
5873
7223
|
|
|
5874
7224
|
// src/response-viewer/clinical-display-data.ts
|
|
5875
7225
|
var BODY_REGION_LABELS = {
|
|
@@ -6490,7 +7840,13 @@ function formatFallbackValue(value) {
|
|
|
6490
7840
|
if (value == null) return "\u2014";
|
|
6491
7841
|
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
6492
7842
|
if (Array.isArray(value)) return value.map(formatFallbackValue).join(", ");
|
|
6493
|
-
if (typeof value === "object")
|
|
7843
|
+
if (typeof value === "object") {
|
|
7844
|
+
try {
|
|
7845
|
+
return JSON.stringify(value, null, 2);
|
|
7846
|
+
} catch {
|
|
7847
|
+
return "[Object]";
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
6494
7850
|
return String(value);
|
|
6495
7851
|
}
|
|
6496
7852
|
function TimelineView({ responses, questions, onSelect }) {
|
|
@@ -7004,26 +8360,11 @@ function exportToJson(responses, filename = "responses.json") {
|
|
|
7004
8360
|
const content = JSON.stringify(responses, null, 2);
|
|
7005
8361
|
downloadBlob(content, filename, "application/json;charset=utf-8;");
|
|
7006
8362
|
}
|
|
7007
|
-
var DISPLAY_ONLY_TYPES2 = /* @__PURE__ */ new Set([
|
|
7008
|
-
"info-block",
|
|
7009
|
-
"info_block",
|
|
7010
|
-
"section-header",
|
|
7011
|
-
"page-break",
|
|
7012
|
-
"image",
|
|
7013
|
-
"divider",
|
|
7014
|
-
"spacer",
|
|
7015
|
-
"video",
|
|
7016
|
-
"rich-text",
|
|
7017
|
-
"welcome-screen",
|
|
7018
|
-
"thank-you-screen",
|
|
7019
|
-
"hidden",
|
|
7020
|
-
"calculated"
|
|
7021
|
-
]);
|
|
7022
8363
|
function getExportableQuestions(schema) {
|
|
7023
8364
|
const questions = [];
|
|
7024
8365
|
for (const section of schema.sections) {
|
|
7025
8366
|
for (const q of section.questions) {
|
|
7026
|
-
if (
|
|
8367
|
+
if (DISPLAY_ONLY_TYPES.has(q.type)) continue;
|
|
7027
8368
|
questions.push(q);
|
|
7028
8369
|
}
|
|
7029
8370
|
}
|
|
@@ -7107,7 +8448,7 @@ function downloadBlob(content, filename, mimeType) {
|
|
|
7107
8448
|
document.body.appendChild(link);
|
|
7108
8449
|
link.click();
|
|
7109
8450
|
document.body.removeChild(link);
|
|
7110
|
-
setTimeout(() => URL.revokeObjectURL(url),
|
|
8451
|
+
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
7111
8452
|
}
|
|
7112
8453
|
|
|
7113
8454
|
// src/response-viewer/pagination-utils.ts
|
|
@@ -7227,14 +8568,15 @@ function applyFilters(responses, state) {
|
|
|
7227
8568
|
function matchesDateRange(submittedAt, range) {
|
|
7228
8569
|
if (!range.from && !range.to) return true;
|
|
7229
8570
|
const submitted = new Date(submittedAt);
|
|
8571
|
+
if (isNaN(submitted.getTime())) return false;
|
|
7230
8572
|
if (range.from) {
|
|
7231
|
-
const from = new Date(range.from);
|
|
7232
|
-
from.
|
|
8573
|
+
const from = /* @__PURE__ */ new Date(range.from + "T00:00:00");
|
|
8574
|
+
if (isNaN(from.getTime())) return false;
|
|
7233
8575
|
if (submitted < from) return false;
|
|
7234
8576
|
}
|
|
7235
8577
|
if (range.to) {
|
|
7236
|
-
const to = new Date(range.to);
|
|
7237
|
-
to.
|
|
8578
|
+
const to = /* @__PURE__ */ new Date(range.to + "T23:59:59.999");
|
|
8579
|
+
if (isNaN(to.getTime())) return false;
|
|
7238
8580
|
if (submitted > to) return false;
|
|
7239
8581
|
}
|
|
7240
8582
|
return true;
|
|
@@ -7345,7 +8687,7 @@ function ResponseViewerInner({
|
|
|
7345
8687
|
[searchedResponses, filterState]
|
|
7346
8688
|
);
|
|
7347
8689
|
const pag = paginate(filteredResponses, currentPage, effectivePageSize);
|
|
7348
|
-
const questions = getAllQuestions(schema);
|
|
8690
|
+
const questions = React.useMemo(() => getAllQuestions(schema), [schema]);
|
|
7349
8691
|
const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
|
|
7350
8692
|
function handleSelect(response) {
|
|
7351
8693
|
setSelectedResponse(response);
|
|
@@ -7794,7 +9136,7 @@ function ResponseViewerInner({
|
|
|
7794
9136
|
response: selectedResponse,
|
|
7795
9137
|
fields: getFields(selectedResponse),
|
|
7796
9138
|
onBack: handleBack,
|
|
7797
|
-
onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
|
|
9139
|
+
onDelete: onDelete && selectedResponse.sessionToken != null ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
|
|
7798
9140
|
}
|
|
7799
9141
|
) }) : viewMode === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
7800
9142
|
TimelineView,
|
|
@@ -7899,26 +9241,11 @@ function ResponseViewerInner({
|
|
|
7899
9241
|
}
|
|
7900
9242
|
);
|
|
7901
9243
|
}
|
|
7902
|
-
var DISPLAY_ONLY_TYPES3 = /* @__PURE__ */ new Set([
|
|
7903
|
-
"info-block",
|
|
7904
|
-
"info_block",
|
|
7905
|
-
"section-header",
|
|
7906
|
-
"page-break",
|
|
7907
|
-
"image",
|
|
7908
|
-
"divider",
|
|
7909
|
-
"spacer",
|
|
7910
|
-
"video",
|
|
7911
|
-
"rich-text",
|
|
7912
|
-
"welcome-screen",
|
|
7913
|
-
"thank-you-screen",
|
|
7914
|
-
"hidden",
|
|
7915
|
-
"calculated"
|
|
7916
|
-
]);
|
|
7917
9244
|
function getAllQuestions(schema) {
|
|
7918
9245
|
const questions = [];
|
|
7919
9246
|
for (const section of schema.sections) {
|
|
7920
9247
|
for (const q of section.questions) {
|
|
7921
|
-
if (
|
|
9248
|
+
if (DISPLAY_ONLY_TYPES.has(q.type)) continue;
|
|
7922
9249
|
questions.push(q);
|
|
7923
9250
|
}
|
|
7924
9251
|
}
|
|
@@ -8344,16 +9671,20 @@ function deepMerge(base, partial) {
|
|
|
8344
9671
|
|
|
8345
9672
|
// src/theme-editor/palette-generator.ts
|
|
8346
9673
|
function generatePalette(baseHex) {
|
|
9674
|
+
const cleaned = baseHex.replace("#", "");
|
|
9675
|
+
if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(cleaned)) {
|
|
9676
|
+
return generatePalette("#6B7280");
|
|
9677
|
+
}
|
|
8347
9678
|
const [h, s, l] = hexToHsl(baseHex);
|
|
8348
9679
|
const secH = (h + 180) % 360;
|
|
8349
9680
|
const isLightBase = l > 50;
|
|
8350
9681
|
return {
|
|
8351
9682
|
// Primary
|
|
8352
9683
|
primary: hslToHex(h, s, clamp(l, 30, 60)),
|
|
8353
|
-
primaryForeground: isLightBase ? "#ffffff" : "#
|
|
9684
|
+
primaryForeground: isLightBase ? "#ffffff" : "#000000",
|
|
8354
9685
|
// Secondary (complementary)
|
|
8355
9686
|
secondary: hslToHex(secH, Math.max(s - 15, 10), clamp(l, 35, 55)),
|
|
8356
|
-
secondaryForeground: "#ffffff",
|
|
9687
|
+
secondaryForeground: isLightBase ? "#ffffff" : "#000000",
|
|
8357
9688
|
// Surfaces
|
|
8358
9689
|
surface: hslToHex(h, Math.max(s - 35, 3), 97),
|
|
8359
9690
|
background: "#F4F7F8",
|
|
@@ -9399,6 +10730,12 @@ function resolveThemeFromDOM() {
|
|
|
9399
10730
|
const val = styles.getPropertyValue(prop).trim();
|
|
9400
10731
|
return val || void 0;
|
|
9401
10732
|
};
|
|
10733
|
+
const getNumber = (prop) => {
|
|
10734
|
+
const val = get(prop);
|
|
10735
|
+
if (val == null) return void 0;
|
|
10736
|
+
const num = parseFloat(val);
|
|
10737
|
+
return isNaN(num) ? void 0 : num;
|
|
10738
|
+
};
|
|
9402
10739
|
return {
|
|
9403
10740
|
colors: {
|
|
9404
10741
|
primary: get("--primary"),
|
|
@@ -9431,6 +10768,17 @@ function resolveThemeFromDOM() {
|
|
|
9431
10768
|
inputRadius: get("--fc-radius-input"),
|
|
9432
10769
|
buttonRadius: get("--fc-radius-button"),
|
|
9433
10770
|
cardRadius: get("--fc-radius-card")
|
|
10771
|
+
},
|
|
10772
|
+
spacing: {
|
|
10773
|
+
base: getNumber("--fc-spacing-base"),
|
|
10774
|
+
sectionGap: getNumber("--fc-section-gap"),
|
|
10775
|
+
fieldGap: getNumber("--fc-field-gap"),
|
|
10776
|
+
inputPaddingX: getNumber("--fc-input-padding-x"),
|
|
10777
|
+
inputPaddingY: getNumber("--fc-input-padding-y")
|
|
10778
|
+
},
|
|
10779
|
+
layout: {
|
|
10780
|
+
maxWidth: get("--fc-max-width")
|
|
10781
|
+
// alignment, progressPosition, sectionLayout are enums — not resolvable from CSS
|
|
9434
10782
|
}
|
|
9435
10783
|
};
|
|
9436
10784
|
}
|
|
@@ -9485,6 +10833,9 @@ function detectPresetFamily(theme) {
|
|
|
9485
10833
|
}
|
|
9486
10834
|
return null;
|
|
9487
10835
|
}
|
|
10836
|
+
function isValidHex(value) {
|
|
10837
|
+
return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
|
|
10838
|
+
}
|
|
9488
10839
|
var SECTIONS = [
|
|
9489
10840
|
{
|
|
9490
10841
|
id: "colors",
|
|
@@ -9735,6 +11086,7 @@ function ThemeEditorInner({
|
|
|
9735
11086
|
a.click();
|
|
9736
11087
|
URL.revokeObjectURL(url);
|
|
9737
11088
|
}, [theme]);
|
|
11089
|
+
const [importError, setImportError] = React.useState(null);
|
|
9738
11090
|
const importJson = React.useCallback(() => {
|
|
9739
11091
|
const input = document.createElement("input");
|
|
9740
11092
|
input.type = "file";
|
|
@@ -9746,10 +11098,16 @@ function ThemeEditorInner({
|
|
|
9746
11098
|
reader.onload = () => {
|
|
9747
11099
|
try {
|
|
9748
11100
|
const parsed = JSON.parse(reader.result);
|
|
11101
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !("colors" in parsed || "typography" in parsed || "shape" in parsed || "spacing" in parsed || "layout" in parsed)) {
|
|
11102
|
+
setImportError("Invalid theme file: expected an object with colors, typography, shape, spacing, or layout.");
|
|
11103
|
+
return;
|
|
11104
|
+
}
|
|
11105
|
+
setImportError(null);
|
|
9749
11106
|
setTheme(parsed);
|
|
9750
11107
|
setPresetKey("custom");
|
|
9751
11108
|
onChange?.(parsed, "custom");
|
|
9752
11109
|
} catch {
|
|
11110
|
+
setImportError("Failed to parse JSON file. Please check the file format.");
|
|
9753
11111
|
}
|
|
9754
11112
|
};
|
|
9755
11113
|
reader.readAsText(file);
|
|
@@ -9827,6 +11185,10 @@ function ThemeEditorInner({
|
|
|
9827
11185
|
onSave && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onSave(theme, presetKey), className: "fcte-btn fcte-btn--primary", children: "Save" })
|
|
9828
11186
|
] })
|
|
9829
11187
|
] }),
|
|
11188
|
+
importError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-import-error", role: "alert", children: [
|
|
11189
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: importError }),
|
|
11190
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setImportError(null), className: "fcte-import-error__close", children: "\xD7" })
|
|
11191
|
+
] }),
|
|
9830
11192
|
showPalette && /* @__PURE__ */ jsxRuntime.jsx(
|
|
9831
11193
|
PaletteGenerator,
|
|
9832
11194
|
{
|
|
@@ -9860,6 +11222,8 @@ function ThemeEditorInner({
|
|
|
9860
11222
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-fields", children: currentSection.fields.map((field) => {
|
|
9861
11223
|
const val = sectionValues[field.key];
|
|
9862
11224
|
if (field.kind === "color") {
|
|
11225
|
+
const colorVal = typeof val === "string" ? val : "#000000";
|
|
11226
|
+
const isInvalid = colorVal.length > 0 && !isValidHex(colorVal);
|
|
9863
11227
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-field", children: [
|
|
9864
11228
|
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-field__label", children: field.label }),
|
|
9865
11229
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-field__color-row", children: [
|
|
@@ -9867,7 +11231,7 @@ function ThemeEditorInner({
|
|
|
9867
11231
|
"input",
|
|
9868
11232
|
{
|
|
9869
11233
|
type: "color",
|
|
9870
|
-
value:
|
|
11234
|
+
value: isValidHex(colorVal) ? colorVal : "#000000",
|
|
9871
11235
|
onChange: (e) => updateField(currentSection.themeKey, field.key, e.target.value),
|
|
9872
11236
|
className: "fcte-field__swatch"
|
|
9873
11237
|
}
|
|
@@ -9876,10 +11240,24 @@ function ThemeEditorInner({
|
|
|
9876
11240
|
"input",
|
|
9877
11241
|
{
|
|
9878
11242
|
type: "text",
|
|
9879
|
-
value:
|
|
9880
|
-
onChange: (e) =>
|
|
9881
|
-
|
|
9882
|
-
|
|
11243
|
+
value: colorVal,
|
|
11244
|
+
onChange: (e) => {
|
|
11245
|
+
const v = e.target.value;
|
|
11246
|
+
if (isValidHex(v)) {
|
|
11247
|
+
updateField(currentSection.themeKey, field.key, v);
|
|
11248
|
+
} else {
|
|
11249
|
+
setTheme((prev) => ({
|
|
11250
|
+
...prev,
|
|
11251
|
+
[currentSection.themeKey]: {
|
|
11252
|
+
...prev[currentSection.themeKey],
|
|
11253
|
+
[field.key]: v
|
|
11254
|
+
}
|
|
11255
|
+
}));
|
|
11256
|
+
}
|
|
11257
|
+
},
|
|
11258
|
+
className: `fcte-field__text${isInvalid ? " fcte-field__text--invalid" : ""}`,
|
|
11259
|
+
spellCheck: false,
|
|
11260
|
+
placeholder: "#RRGGBB"
|
|
9883
11261
|
}
|
|
9884
11262
|
)
|
|
9885
11263
|
] })
|
|
@@ -9908,9 +11286,16 @@ function ThemeEditorInner({
|
|
|
9908
11286
|
{
|
|
9909
11287
|
type: "number",
|
|
9910
11288
|
value: typeof val === "number" ? val : 0,
|
|
9911
|
-
onChange: (e) =>
|
|
11289
|
+
onChange: (e) => {
|
|
11290
|
+
let n = Math.round(Number(e.target.value));
|
|
11291
|
+
if (isNaN(n)) n = 0;
|
|
11292
|
+
if (field.min != null) n = Math.max(field.min, n);
|
|
11293
|
+
if (field.max != null) n = Math.min(field.max, n);
|
|
11294
|
+
updateField(currentSection.themeKey, field.key, n);
|
|
11295
|
+
},
|
|
9912
11296
|
min: field.min,
|
|
9913
11297
|
max: field.max,
|
|
11298
|
+
step: 1,
|
|
9914
11299
|
className: "fcte-field__number"
|
|
9915
11300
|
}
|
|
9916
11301
|
),
|
|
@@ -9928,7 +11313,8 @@ function ThemeEditorInner({
|
|
|
9928
11313
|
onChange: (e) => updateField(currentSection.themeKey, field.key, e.target.value),
|
|
9929
11314
|
placeholder: field.placeholder,
|
|
9930
11315
|
className: "fcte-field__text",
|
|
9931
|
-
spellCheck: false
|
|
11316
|
+
spellCheck: false,
|
|
11317
|
+
maxLength: 200
|
|
9932
11318
|
}
|
|
9933
11319
|
)
|
|
9934
11320
|
] }, field.key);
|
|
@@ -10366,6 +11752,7 @@ var consultationBookingSchema = {
|
|
|
10366
11752
|
required: true,
|
|
10367
11753
|
config: {
|
|
10368
11754
|
type: "appointment",
|
|
11755
|
+
// Example slots — replace with dynamic data or a slots URL in production
|
|
10369
11756
|
slots: [
|
|
10370
11757
|
{
|
|
10371
11758
|
date: "2026-09-01",
|
|
@@ -10466,6 +11853,55 @@ var consultationBookingSchema = {
|
|
|
10466
11853
|
type: "divider",
|
|
10467
11854
|
label: ""
|
|
10468
11855
|
},
|
|
11856
|
+
{
|
|
11857
|
+
id: "hourly_rate",
|
|
11858
|
+
type: "calculated",
|
|
11859
|
+
label: "Hourly Rate",
|
|
11860
|
+
config: {
|
|
11861
|
+
type: "calculated",
|
|
11862
|
+
expression: 'IF({service_type} = "legal", 150, IF({service_type} = "business", 200, IF({service_type} = "technical", 175, 0)))',
|
|
11863
|
+
format: "currency",
|
|
11864
|
+
decimalPlaces: 2,
|
|
11865
|
+
prefix: "$"
|
|
11866
|
+
}
|
|
11867
|
+
},
|
|
11868
|
+
{
|
|
11869
|
+
id: "session_cost",
|
|
11870
|
+
type: "calculated",
|
|
11871
|
+
label: "Session Cost",
|
|
11872
|
+
helpText: "Based on service type and session duration",
|
|
11873
|
+
config: {
|
|
11874
|
+
type: "calculated",
|
|
11875
|
+
expression: "{hourly_rate} * {session_duration} / 60",
|
|
11876
|
+
format: "currency",
|
|
11877
|
+
decimalPlaces: 2,
|
|
11878
|
+
prefix: "$"
|
|
11879
|
+
}
|
|
11880
|
+
},
|
|
11881
|
+
{
|
|
11882
|
+
id: "rush_fee",
|
|
11883
|
+
type: "calculated",
|
|
11884
|
+
label: "Rush Fee",
|
|
11885
|
+
config: {
|
|
11886
|
+
type: "calculated",
|
|
11887
|
+
expression: 'IF({urgency} = "rush", 50, 0)',
|
|
11888
|
+
format: "currency",
|
|
11889
|
+
decimalPlaces: 2,
|
|
11890
|
+
prefix: "$"
|
|
11891
|
+
}
|
|
11892
|
+
},
|
|
11893
|
+
{
|
|
11894
|
+
id: "total_cost",
|
|
11895
|
+
type: "calculated",
|
|
11896
|
+
label: "Total",
|
|
11897
|
+
config: {
|
|
11898
|
+
type: "calculated",
|
|
11899
|
+
expression: "{session_cost} + {rush_fee}",
|
|
11900
|
+
format: "currency",
|
|
11901
|
+
decimalPlaces: 2,
|
|
11902
|
+
prefix: "$"
|
|
11903
|
+
}
|
|
11904
|
+
},
|
|
10469
11905
|
{
|
|
10470
11906
|
id: "promo_code",
|
|
10471
11907
|
type: "short_text",
|
|
@@ -10485,10 +11921,10 @@ var consultationBookingSchema = {
|
|
|
10485
11921
|
provider: "stripe",
|
|
10486
11922
|
publicKey: "",
|
|
10487
11923
|
// Set your Stripe publishable key (pk_test_... or pk_live_...)
|
|
10488
|
-
|
|
11924
|
+
amountField: "total_cost",
|
|
10489
11925
|
currency: "USD",
|
|
10490
11926
|
description: "Consultation Session"
|
|
10491
|
-
//
|
|
11927
|
+
// serverUrl: "" — developer must configure their own payment intent endpoint
|
|
10492
11928
|
}
|
|
10493
11929
|
}
|
|
10494
11930
|
]
|
|
@@ -10985,13 +12421,50 @@ var ecommerceCheckoutSchema = {
|
|
|
10985
12421
|
prefix: "$"
|
|
10986
12422
|
}
|
|
10987
12423
|
},
|
|
12424
|
+
{
|
|
12425
|
+
id: "shipping_cost",
|
|
12426
|
+
type: "calculated",
|
|
12427
|
+
label: "Shipping",
|
|
12428
|
+
helpText: "Based on your selected shipping method",
|
|
12429
|
+
config: {
|
|
12430
|
+
type: "calculated",
|
|
12431
|
+
expression: 'IF({shipping_method} = "overnight", 29.99, IF({shipping_method} = "express", 14.99, IF({shipping_method} = "standard", 7.99, 0)))',
|
|
12432
|
+
format: "currency",
|
|
12433
|
+
decimalPlaces: 2,
|
|
12434
|
+
prefix: "$"
|
|
12435
|
+
}
|
|
12436
|
+
},
|
|
12437
|
+
{
|
|
12438
|
+
id: "insurance_cost",
|
|
12439
|
+
type: "calculated",
|
|
12440
|
+
label: "Shipping Insurance",
|
|
12441
|
+
config: {
|
|
12442
|
+
type: "calculated",
|
|
12443
|
+
expression: "IF({shipping_insurance} = true, 4.99, 0)",
|
|
12444
|
+
format: "currency",
|
|
12445
|
+
decimalPlaces: 2,
|
|
12446
|
+
prefix: "$"
|
|
12447
|
+
}
|
|
12448
|
+
},
|
|
12449
|
+
{
|
|
12450
|
+
id: "gift_wrap_cost",
|
|
12451
|
+
type: "calculated",
|
|
12452
|
+
label: "Gift Wrapping",
|
|
12453
|
+
config: {
|
|
12454
|
+
type: "calculated",
|
|
12455
|
+
expression: "IF({gift_wrap} = true, 5.99, 0)",
|
|
12456
|
+
format: "currency",
|
|
12457
|
+
decimalPlaces: 2,
|
|
12458
|
+
prefix: "$"
|
|
12459
|
+
}
|
|
12460
|
+
},
|
|
10988
12461
|
{
|
|
10989
12462
|
id: "order_total",
|
|
10990
12463
|
type: "calculated",
|
|
10991
12464
|
label: "Order Total",
|
|
10992
12465
|
config: {
|
|
10993
12466
|
type: "calculated",
|
|
10994
|
-
expression: "{order_subtotal} + {order_tax}",
|
|
12467
|
+
expression: "{order_subtotal} + {order_tax} + {shipping_cost} + {insurance_cost} + {gift_wrap_cost}",
|
|
10995
12468
|
format: "currency",
|
|
10996
12469
|
decimalPlaces: 2,
|
|
10997
12470
|
prefix: "$"
|
|
@@ -11013,10 +12486,10 @@ var ecommerceCheckoutSchema = {
|
|
|
11013
12486
|
provider: "stripe",
|
|
11014
12487
|
publicKey: "",
|
|
11015
12488
|
// Set your Stripe publishable key (pk_test_... or pk_live_...)
|
|
11016
|
-
|
|
12489
|
+
amountField: "order_total",
|
|
11017
12490
|
currency: "USD",
|
|
11018
12491
|
description: "E-commerce Order"
|
|
11019
|
-
//
|
|
12492
|
+
// serverUrl: "" — developer must configure their own payment intent endpoint
|
|
11020
12493
|
}
|
|
11021
12494
|
}
|
|
11022
12495
|
]
|
|
@@ -11122,6 +12595,8 @@ exports.FormBuilderThemeProvider = FormBuilderThemeProvider;
|
|
|
11122
12595
|
exports.PRESET_FAMILIES = PRESET_FAMILIES;
|
|
11123
12596
|
exports.PREVIEW_SCHEMA = PREVIEW_SCHEMA;
|
|
11124
12597
|
exports.PRO_FIELD_OVERRIDES = PRO_FIELD_OVERRIDES;
|
|
12598
|
+
exports.ProAppointmentField = ProAppointmentField;
|
|
12599
|
+
exports.ProFileUploadField = ProFileUploadField;
|
|
11125
12600
|
exports.ProPaymentField = ProPaymentField;
|
|
11126
12601
|
exports.QUESTION_TYPE_INFO = QUESTION_TYPE_INFO;
|
|
11127
12602
|
exports.ResponseViewer = ResponseViewer;
|
|
@@ -11135,13 +12610,18 @@ exports.cn = cn;
|
|
|
11135
12610
|
exports.consultationBooking = consultationBooking;
|
|
11136
12611
|
exports.consultationBookingMeta = consultationBookingMeta;
|
|
11137
12612
|
exports.consultationBookingSchema = consultationBookingSchema;
|
|
12613
|
+
exports.decipherPresignPayload = decipherPresignPayload;
|
|
12614
|
+
exports.decryptEphemeralPayload = decryptEphemeralPayload;
|
|
11138
12615
|
exports.duplicateQuestion = duplicateQuestion;
|
|
11139
12616
|
exports.duplicateSection = duplicateSection;
|
|
11140
12617
|
exports.ecommerceCheckout = ecommerceCheckout;
|
|
11141
12618
|
exports.ecommerceCheckoutMeta = ecommerceCheckoutMeta;
|
|
11142
12619
|
exports.ecommerceCheckoutSchema = ecommerceCheckoutSchema;
|
|
12620
|
+
exports.encryptPresignPayload = encryptPresignPayload;
|
|
12621
|
+
exports.exportPublicKeySpki = exportPublicKeySpki;
|
|
11143
12622
|
exports.findQuestion = findQuestion;
|
|
11144
12623
|
exports.findSection = findSection;
|
|
12624
|
+
exports.generateEphemeralKeyPair = generateEphemeralKeyPair;
|
|
11145
12625
|
exports.generateId = generateId;
|
|
11146
12626
|
exports.generateOptionId = generateOptionId;
|
|
11147
12627
|
exports.generateQuestionId = generateQuestionId;
|