@sunboard/react 0.1.2 → 0.2.0
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/index.d.mts +79 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +390 -28
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +773 -560
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -142,7 +142,10 @@ function isAllowedLocalPreviewUrl(url) {
|
|
|
142
142
|
function isSunboardSpecLike(value) {
|
|
143
143
|
if (!value || typeof value !== "object") return false;
|
|
144
144
|
const candidate = value;
|
|
145
|
-
|
|
145
|
+
if (typeof candidate.key !== "string" || typeof candidate.name !== "string" || typeof candidate.type !== "string") return false;
|
|
146
|
+
const hasChecklist = typeof candidate.checklist === "object" && Array.isArray(candidate.checklist?.steps);
|
|
147
|
+
const hasSurvey = typeof candidate.survey === "object" && Array.isArray(candidate.survey?.steps);
|
|
148
|
+
return hasChecklist || hasSurvey;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
//#endregion
|
|
@@ -163,27 +166,44 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
163
166
|
sdkVersion: SDK_VERSION
|
|
164
167
|
});
|
|
165
168
|
}, [apiUrl, publishableKey]);
|
|
169
|
+
const [bootstrapNonce, setBootstrapNonce] = useState(0);
|
|
170
|
+
const refreshBootstrap = useCallback(() => {
|
|
171
|
+
setBootstrapNonce((nonce) => nonce + 1);
|
|
172
|
+
}, []);
|
|
166
173
|
const bootstrap = useSunboardBootstrap(client, {
|
|
167
174
|
enabled: !specProp && preview.source !== "preview" && !preview.loading,
|
|
168
175
|
environment,
|
|
176
|
+
nonce: bootstrapNonce,
|
|
169
177
|
origin: currentOrigin,
|
|
170
178
|
route: currentRoute,
|
|
171
179
|
user
|
|
172
180
|
});
|
|
173
181
|
const activeExperience = bootstrap.experience;
|
|
174
|
-
const
|
|
182
|
+
const activeSurvey = bootstrap.survey;
|
|
183
|
+
const hostedSpec = activeSurvey?.spec ?? activeExperience?.spec ?? null;
|
|
175
184
|
const resolvedSpec = specProp ?? preview.spec ?? hostedSpec;
|
|
176
185
|
const resolvedLoading = loadingProp ?? (preview.loading || bootstrap.loading);
|
|
177
186
|
const resolvedMode = modeProp ?? (preview.source === "preview" ? "preview" : "production");
|
|
178
187
|
const hostedMode = client !== null && !specProp && preview.source !== "preview";
|
|
188
|
+
const ownedAttributes = bootstrap.attributes;
|
|
189
|
+
const effectiveUser = useMemo(() => {
|
|
190
|
+
if (!user || !ownedAttributes || Object.keys(ownedAttributes).length === 0) return user;
|
|
191
|
+
return {
|
|
192
|
+
...user,
|
|
193
|
+
properties: {
|
|
194
|
+
...user.properties ?? {},
|
|
195
|
+
...ownedAttributes
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}, [user, ownedAttributes]);
|
|
179
199
|
const [completedStepKeys, setCompletedStepKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
180
200
|
const [completedTourKeys, setCompletedTourKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
181
201
|
const [activeTourState, setActiveTourState] = useState(null);
|
|
182
202
|
const [tourError, setTourError] = useState(null);
|
|
183
203
|
const lastNavigatedTourStep = useRef(null);
|
|
184
204
|
const storageKey = !hostedMode && resolvedSpec && user ? completedToursStorageKey(resolvedSpec.key, user.id) : null;
|
|
185
|
-
const experienceKey = activeExperience?.key ?? resolvedSpec?.key ?? null;
|
|
186
|
-
const versionId = activeExperience?.version.id ?? null;
|
|
205
|
+
const experienceKey = activeSurvey?.key ?? activeExperience?.key ?? resolvedSpec?.key ?? null;
|
|
206
|
+
const versionId = activeSurvey?.version.id ?? activeExperience?.version.id ?? null;
|
|
187
207
|
useEffect(() => {
|
|
188
208
|
setCompletedStepKeys(/* @__PURE__ */ new Set());
|
|
189
209
|
setActiveTourState(null);
|
|
@@ -221,7 +241,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
221
241
|
]);
|
|
222
242
|
useEffect(() => {
|
|
223
243
|
if (!resolvedSpec) return;
|
|
224
|
-
const tourCompletedStepKeys = resolvedSpec.checklist
|
|
244
|
+
const tourCompletedStepKeys = (resolvedSpec.checklist?.steps ?? []).filter((step) => step.completeWhen?.tour && effectiveCompletedTourKeys.has(step.completeWhen.tour)).map((step) => step.key);
|
|
225
245
|
if (tourCompletedStepKeys.length === 0) return;
|
|
226
246
|
setCompletedStepKeys((current) => mergeKeys(current, tourCompletedStepKeys));
|
|
227
247
|
}, [effectiveCompletedTourKeys, resolvedSpec]);
|
|
@@ -270,8 +290,8 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
270
290
|
hostedMode,
|
|
271
291
|
user?.id
|
|
272
292
|
]);
|
|
273
|
-
const totalSteps = resolvedSpec?.checklist
|
|
274
|
-
const completedCount = resolvedSpec?.checklist
|
|
293
|
+
const totalSteps = resolvedSpec?.checklist?.steps.length ?? 0;
|
|
294
|
+
const completedCount = resolvedSpec?.checklist?.steps.filter((step) => effectiveCompletedStepKeys.has(step.key)).length ?? 0;
|
|
275
295
|
const completeTour = useCallback((tourKey) => {
|
|
276
296
|
setCompletedTourKeys((current) => {
|
|
277
297
|
const next = new Set(current);
|
|
@@ -289,7 +309,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
289
309
|
ok: false,
|
|
290
310
|
reason: "missing-user"
|
|
291
311
|
};
|
|
292
|
-
const matchingStepKeys = resolvedSpec.checklist
|
|
312
|
+
const matchingStepKeys = (resolvedSpec.checklist?.steps ?? []).filter((step) => step.completeWhen?.event === eventName).map((step) => step.key);
|
|
293
313
|
if (matchingStepKeys.length > 0) setCompletedStepKeys((current) => mergeKeys(current, matchingStepKeys));
|
|
294
314
|
let networkOk = true;
|
|
295
315
|
if (hostedMode && client && experienceKey) try {
|
|
@@ -332,6 +352,56 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
332
352
|
user,
|
|
333
353
|
versionId
|
|
334
354
|
]);
|
|
355
|
+
const submitSurvey = useCallback(async (answers) => {
|
|
356
|
+
if (resolvedLoading || !resolvedSpec) return {
|
|
357
|
+
ok: false,
|
|
358
|
+
reason: "not-ready"
|
|
359
|
+
};
|
|
360
|
+
if (!user) return {
|
|
361
|
+
ok: false,
|
|
362
|
+
reason: "missing-user"
|
|
363
|
+
};
|
|
364
|
+
if (resolvedSpec.type !== "survey" || !resolvedSpec.survey) return {
|
|
365
|
+
ok: false,
|
|
366
|
+
reason: "not-survey"
|
|
367
|
+
};
|
|
368
|
+
if (resolvedMode === "preview") return { ok: true };
|
|
369
|
+
if (!hostedMode || !client || !experienceKey) return {
|
|
370
|
+
ok: false,
|
|
371
|
+
reason: "not-ready"
|
|
372
|
+
};
|
|
373
|
+
try {
|
|
374
|
+
await client.submitSurvey({
|
|
375
|
+
answers,
|
|
376
|
+
environment,
|
|
377
|
+
experienceKey,
|
|
378
|
+
origin: currentOrigin,
|
|
379
|
+
route: currentRoute,
|
|
380
|
+
user: toRuntimeUser(user),
|
|
381
|
+
versionId: versionId ?? void 0
|
|
382
|
+
});
|
|
383
|
+
} catch {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
reason: "network-error"
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
refreshBootstrap();
|
|
390
|
+
return { ok: true };
|
|
391
|
+
}, [
|
|
392
|
+
client,
|
|
393
|
+
currentOrigin,
|
|
394
|
+
currentRoute,
|
|
395
|
+
environment,
|
|
396
|
+
experienceKey,
|
|
397
|
+
hostedMode,
|
|
398
|
+
refreshBootstrap,
|
|
399
|
+
resolvedLoading,
|
|
400
|
+
resolvedMode,
|
|
401
|
+
resolvedSpec,
|
|
402
|
+
user,
|
|
403
|
+
versionId
|
|
404
|
+
]);
|
|
335
405
|
const navigateToStep = useCallback(async (step) => {
|
|
336
406
|
if (!step.route) return;
|
|
337
407
|
await navigateWithFallback(step.route, navigate);
|
|
@@ -377,12 +447,35 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
377
447
|
user,
|
|
378
448
|
versionId
|
|
379
449
|
]);
|
|
380
|
-
const dismissTour = useCallback(async () => {
|
|
450
|
+
const dismissTour = useCallback(async (options) => {
|
|
451
|
+
const dismissed = activeTourState;
|
|
452
|
+
const dismissedStepKey = dismissed ? resolvedSpec?.tours[dismissed.key]?.steps[dismissed.stepIndex]?.key : void 0;
|
|
381
453
|
setActiveTourState(null);
|
|
382
454
|
setTourError(null);
|
|
383
455
|
lastNavigatedTourStep.current = null;
|
|
456
|
+
if (!options?.silent && hostedMode && client && experienceKey && user && dismissed && dismissedStepKey) client.dismissTour({
|
|
457
|
+
environment,
|
|
458
|
+
experienceKey,
|
|
459
|
+
origin: currentOrigin,
|
|
460
|
+
route: currentRoute,
|
|
461
|
+
stepKey: dismissedStepKey,
|
|
462
|
+
tourKey: dismissed.key,
|
|
463
|
+
user: toRuntimeUser(user),
|
|
464
|
+
versionId: versionId ?? void 0
|
|
465
|
+
}).catch(() => {});
|
|
384
466
|
return { ok: true };
|
|
385
|
-
}, [
|
|
467
|
+
}, [
|
|
468
|
+
activeTourState,
|
|
469
|
+
client,
|
|
470
|
+
currentOrigin,
|
|
471
|
+
currentRoute,
|
|
472
|
+
environment,
|
|
473
|
+
experienceKey,
|
|
474
|
+
hostedMode,
|
|
475
|
+
resolvedSpec,
|
|
476
|
+
user,
|
|
477
|
+
versionId
|
|
478
|
+
]);
|
|
386
479
|
const finishActiveTour = useCallback(async (tourKey) => {
|
|
387
480
|
completeTour(tourKey);
|
|
388
481
|
if (!hostedMode || !client || !experienceKey || !user) return;
|
|
@@ -463,7 +556,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
463
556
|
}, []);
|
|
464
557
|
const value = useMemo(() => ({
|
|
465
558
|
spec: resolvedSpec,
|
|
466
|
-
user,
|
|
559
|
+
user: effectiveUser,
|
|
467
560
|
loading: resolvedLoading,
|
|
468
561
|
mode: resolvedMode,
|
|
469
562
|
route: currentRoute,
|
|
@@ -474,6 +567,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
474
567
|
activeTour,
|
|
475
568
|
tourError,
|
|
476
569
|
track,
|
|
570
|
+
submitSurvey,
|
|
477
571
|
startTour,
|
|
478
572
|
dismissTour,
|
|
479
573
|
advanceTour,
|
|
@@ -483,7 +577,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
483
577
|
visibleHotspots
|
|
484
578
|
}), [
|
|
485
579
|
resolvedSpec,
|
|
486
|
-
|
|
580
|
+
effectiveUser,
|
|
487
581
|
resolvedLoading,
|
|
488
582
|
resolvedMode,
|
|
489
583
|
currentRoute,
|
|
@@ -494,6 +588,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
494
588
|
activeTour,
|
|
495
589
|
tourError,
|
|
496
590
|
track,
|
|
591
|
+
submitSurvey,
|
|
497
592
|
startTour,
|
|
498
593
|
dismissTour,
|
|
499
594
|
advanceTour,
|
|
@@ -508,24 +603,30 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, mode: mo
|
|
|
508
603
|
});
|
|
509
604
|
}
|
|
510
605
|
function useSunboardBootstrap(client, options) {
|
|
511
|
-
const { enabled, environment, origin, route, user } = options;
|
|
606
|
+
const { enabled, environment, nonce, origin, route, user } = options;
|
|
512
607
|
const [state, setState] = useState({
|
|
608
|
+
attributes: null,
|
|
513
609
|
experience: null,
|
|
514
|
-
loading: false
|
|
610
|
+
loading: false,
|
|
611
|
+
survey: null
|
|
515
612
|
});
|
|
516
613
|
const warnedSdkAdvisory = useRef(false);
|
|
517
614
|
useEffect(() => {
|
|
518
|
-
if (!client || !enabled || !user?.id) {
|
|
615
|
+
if (!client || !enabled || !user?.id || isPreviewPage()) {
|
|
519
616
|
setState({
|
|
617
|
+
attributes: null,
|
|
520
618
|
experience: null,
|
|
521
|
-
loading: false
|
|
619
|
+
loading: false,
|
|
620
|
+
survey: null
|
|
522
621
|
});
|
|
523
622
|
return;
|
|
524
623
|
}
|
|
525
624
|
const controller = new AbortController();
|
|
526
625
|
setState({
|
|
626
|
+
attributes: null,
|
|
527
627
|
experience: null,
|
|
528
|
-
loading: true
|
|
628
|
+
loading: true,
|
|
629
|
+
survey: null
|
|
529
630
|
});
|
|
530
631
|
client.bootstrap({
|
|
531
632
|
environment,
|
|
@@ -534,21 +635,25 @@ function useSunboardBootstrap(client, options) {
|
|
|
534
635
|
user: toRuntimeUser(user)
|
|
535
636
|
}).then((response) => {
|
|
536
637
|
if (controller.signal.aborted) return;
|
|
537
|
-
if (response.experiences.length === 0 && isDevEnvironment()) console.warn(`[sunboard] bootstrap returned no active experiences for route "${route}" in environment "${environment}". Check the dashboard for an enabled deployment whose trigger.route matches.`);
|
|
638
|
+
if (response.experiences.length === 0 && !response.survey && isDevEnvironment()) console.warn(`[sunboard] bootstrap returned no active experiences for route "${route}" in environment "${environment}". Check the dashboard for an enabled deployment whose trigger.route matches.`);
|
|
538
639
|
if (response.sdkAdvisory && isDevEnvironment() && !warnedSdkAdvisory.current) {
|
|
539
640
|
warnedSdkAdvisory.current = true;
|
|
540
641
|
console.warn(`[sunboard] ${response.sdkAdvisory.message}`);
|
|
541
642
|
}
|
|
542
643
|
setState({
|
|
644
|
+
attributes: response.attributes ?? null,
|
|
543
645
|
experience: response.experiences[0] ?? null,
|
|
544
|
-
loading: false
|
|
646
|
+
loading: false,
|
|
647
|
+
survey: response.survey ?? null
|
|
545
648
|
});
|
|
546
649
|
}).catch((error) => {
|
|
547
650
|
if (controller.signal.aborted) return;
|
|
548
651
|
if (isDevEnvironment()) console.warn("[sunboard] bootstrap failed", error);
|
|
549
652
|
setState({
|
|
653
|
+
attributes: null,
|
|
550
654
|
experience: null,
|
|
551
|
-
loading: false
|
|
655
|
+
loading: false,
|
|
656
|
+
survey: null
|
|
552
657
|
});
|
|
553
658
|
});
|
|
554
659
|
return () => {
|
|
@@ -558,6 +663,7 @@ function useSunboardBootstrap(client, options) {
|
|
|
558
663
|
client,
|
|
559
664
|
enabled,
|
|
560
665
|
environment,
|
|
666
|
+
nonce,
|
|
561
667
|
origin,
|
|
562
668
|
user?.id
|
|
563
669
|
]);
|
|
@@ -578,6 +684,10 @@ function isDevEnvironment() {
|
|
|
578
684
|
if (typeof process === "undefined" || !process.env) return false;
|
|
579
685
|
return process.env.NODE_ENV !== "production";
|
|
580
686
|
}
|
|
687
|
+
function isPreviewPage() {
|
|
688
|
+
if (typeof window === "undefined") return false;
|
|
689
|
+
return new URLSearchParams(window.location.search).get("sunboard_preview") === "1";
|
|
690
|
+
}
|
|
581
691
|
function toRuntimeUser(user) {
|
|
582
692
|
return {
|
|
583
693
|
email: user.email ?? void 0,
|
|
@@ -667,7 +777,7 @@ function actionLabelFor(step) {
|
|
|
667
777
|
if (step.action?.type === "startTour") return "Start";
|
|
668
778
|
if (step.action || step.route) return "Open";
|
|
669
779
|
}
|
|
670
|
-
function interpolateOptional(text, user) {
|
|
780
|
+
function interpolateOptional$1(text, user) {
|
|
671
781
|
if (!text) return void 0;
|
|
672
782
|
return interpolateTokens(text, user) || void 0;
|
|
673
783
|
}
|
|
@@ -689,17 +799,17 @@ function useChecklist() {
|
|
|
689
799
|
const sawIncompleteFirst = observationRef.current?.sawIncomplete ?? false;
|
|
690
800
|
const dismissed = dismissedKey !== null && dismissedKey === observationKey;
|
|
691
801
|
const rawSteps = useMemo(() => spec?.checklist?.steps ?? [], [spec]);
|
|
692
|
-
const title = useMemo(() => interpolateOptional(spec?.title, user ?? null), [spec?.title, user]);
|
|
693
|
-
const subtitle = useMemo(() => interpolateOptional(spec?.subtitle, user ?? null), [spec?.subtitle, user]);
|
|
802
|
+
const title = useMemo(() => interpolateOptional$1(spec?.title, user ?? null), [spec?.title, user]);
|
|
803
|
+
const subtitle = useMemo(() => interpolateOptional$1(spec?.subtitle, user ?? null), [spec?.subtitle, user]);
|
|
694
804
|
const completion = useMemo(() => {
|
|
695
805
|
const raw = spec?.completion;
|
|
696
806
|
if (!raw) return void 0;
|
|
697
|
-
const interpolatedTitle = interpolateOptional(raw.title, user ?? null);
|
|
807
|
+
const interpolatedTitle = interpolateOptional$1(raw.title, user ?? null);
|
|
698
808
|
if (!interpolatedTitle) return void 0;
|
|
699
809
|
return {
|
|
700
810
|
title: interpolatedTitle,
|
|
701
|
-
subtitle: interpolateOptional(raw.subtitle, user ?? null),
|
|
702
|
-
message: interpolateOptional(raw.message, user ?? null),
|
|
811
|
+
subtitle: interpolateOptional$1(raw.subtitle, user ?? null),
|
|
812
|
+
message: interpolateOptional$1(raw.message, user ?? null),
|
|
703
813
|
dismissAfterMs: raw.dismissAfterMs
|
|
704
814
|
};
|
|
705
815
|
}, [spec?.completion, user]);
|
|
@@ -745,6 +855,140 @@ function useChecklist() {
|
|
|
745
855
|
};
|
|
746
856
|
}
|
|
747
857
|
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/hooks/useSurvey.ts
|
|
860
|
+
function interpolateOptional(text, user) {
|
|
861
|
+
if (!text) return void 0;
|
|
862
|
+
return interpolateTokens(text, user) || void 0;
|
|
863
|
+
}
|
|
864
|
+
function answered(value) {
|
|
865
|
+
if (value === void 0 || value === null) return false;
|
|
866
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
867
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
function useSurvey() {
|
|
871
|
+
const { spec, user, loading, submitSurvey } = useSunboard();
|
|
872
|
+
const [answers, setAnswers] = useState({});
|
|
873
|
+
const [submitting, setSubmitting] = useState(false);
|
|
874
|
+
const [submitted, setSubmitted] = useState(false);
|
|
875
|
+
const [error, setError] = useState(null);
|
|
876
|
+
const [stepIndex, setStepIndex] = useState(0);
|
|
877
|
+
const isSurvey = Boolean(spec && spec.type === "survey" && spec.survey);
|
|
878
|
+
const ready = !loading && !!user && isSurvey;
|
|
879
|
+
const title = useMemo(() => interpolateOptional(spec?.title, user ?? null), [spec?.title, user]);
|
|
880
|
+
const subtitle = useMemo(() => interpolateOptional(spec?.subtitle, user ?? null), [spec?.subtitle, user]);
|
|
881
|
+
const steps = useMemo(() => {
|
|
882
|
+
return (spec?.survey?.steps ?? []).map((step) => ({
|
|
883
|
+
title: interpolateOptional(step.title, user ?? null),
|
|
884
|
+
subtitle: interpolateOptional(step.subtitle, user ?? null),
|
|
885
|
+
questions: step.questions.map((question) => ({
|
|
886
|
+
key: question.key,
|
|
887
|
+
prompt: interpolateTokens(question.prompt, user ?? null),
|
|
888
|
+
type: question.type,
|
|
889
|
+
required: question.required,
|
|
890
|
+
options: (question.options ?? []).map((option) => ({
|
|
891
|
+
value: option.value,
|
|
892
|
+
label: interpolateTokens(option.label, user ?? null)
|
|
893
|
+
}))
|
|
894
|
+
}))
|
|
895
|
+
}));
|
|
896
|
+
}, [spec?.survey, user]);
|
|
897
|
+
const questions = useMemo(() => steps.flatMap((step) => step.questions), [steps]);
|
|
898
|
+
const signature = questions.map((question) => question.key).join("|");
|
|
899
|
+
const signatureRef = useRef(signature);
|
|
900
|
+
if (signatureRef.current !== signature) {
|
|
901
|
+
signatureRef.current = signature;
|
|
902
|
+
if (stepIndex !== 0) setStepIndex(0);
|
|
903
|
+
}
|
|
904
|
+
const totalSteps = steps.length;
|
|
905
|
+
const safeIndex = totalSteps > 0 ? Math.min(stepIndex, totalSteps - 1) : 0;
|
|
906
|
+
const currentStep = steps[safeIndex];
|
|
907
|
+
const setAnswer = useCallback((key, value) => {
|
|
908
|
+
setAnswers((current) => ({
|
|
909
|
+
...current,
|
|
910
|
+
[key]: value
|
|
911
|
+
}));
|
|
912
|
+
}, []);
|
|
913
|
+
const toggleAnswer = useCallback((key, value) => {
|
|
914
|
+
setAnswers((current) => {
|
|
915
|
+
const existing = Array.isArray(current[key]) ? current[key] : [];
|
|
916
|
+
const next$1 = existing.includes(value) ? existing.filter((item) => item !== value) : [...existing, value];
|
|
917
|
+
return {
|
|
918
|
+
...current,
|
|
919
|
+
[key]: next$1
|
|
920
|
+
};
|
|
921
|
+
});
|
|
922
|
+
}, []);
|
|
923
|
+
const isAnswered = useCallback((key) => answered(answers[key]), [answers]);
|
|
924
|
+
const canSubmit = useMemo(() => {
|
|
925
|
+
if (!ready || submitting) return false;
|
|
926
|
+
return questions.filter((question) => question.required).every((question) => answered(answers[question.key]));
|
|
927
|
+
}, [
|
|
928
|
+
answers,
|
|
929
|
+
questions,
|
|
930
|
+
ready,
|
|
931
|
+
submitting
|
|
932
|
+
]);
|
|
933
|
+
const canAdvance = useMemo(() => {
|
|
934
|
+
if (!currentStep) return false;
|
|
935
|
+
return currentStep.questions.filter((question) => question.required).every((question) => answered(answers[question.key]));
|
|
936
|
+
}, [answers, currentStep]);
|
|
937
|
+
const submit = useCallback(async () => {
|
|
938
|
+
if (!canSubmit) return;
|
|
939
|
+
setSubmitting(true);
|
|
940
|
+
setError(null);
|
|
941
|
+
const result = await submitSurvey(answers);
|
|
942
|
+
setSubmitting(false);
|
|
943
|
+
if (result.ok) {
|
|
944
|
+
setSubmitted(true);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
setError(result.reason === "network-error" ? "Something went wrong. Please try again." : "Survey is not ready.");
|
|
948
|
+
}, [
|
|
949
|
+
answers,
|
|
950
|
+
canSubmit,
|
|
951
|
+
submitSurvey
|
|
952
|
+
]);
|
|
953
|
+
const isLastStep = totalSteps === 0 || safeIndex >= totalSteps - 1;
|
|
954
|
+
const isFirstStep = safeIndex === 0;
|
|
955
|
+
const next = useCallback(() => {
|
|
956
|
+
if (isLastStep) submit();
|
|
957
|
+
else setStepIndex(safeIndex + 1);
|
|
958
|
+
}, [
|
|
959
|
+
isLastStep,
|
|
960
|
+
safeIndex,
|
|
961
|
+
submit
|
|
962
|
+
]);
|
|
963
|
+
const back = useCallback(() => {
|
|
964
|
+
setStepIndex((current) => Math.max(0, current - 1));
|
|
965
|
+
}, []);
|
|
966
|
+
return {
|
|
967
|
+
visible: ready && !submitted,
|
|
968
|
+
title,
|
|
969
|
+
subtitle,
|
|
970
|
+
questions,
|
|
971
|
+
steps,
|
|
972
|
+
stepIndex: safeIndex,
|
|
973
|
+
totalSteps,
|
|
974
|
+
currentStep,
|
|
975
|
+
progress: totalSteps > 0 ? (safeIndex + 1) / totalSteps * 100 : 0,
|
|
976
|
+
isFirstStep,
|
|
977
|
+
isLastStep,
|
|
978
|
+
canAdvance,
|
|
979
|
+
next,
|
|
980
|
+
back,
|
|
981
|
+
answers,
|
|
982
|
+
setAnswer,
|
|
983
|
+
toggleAnswer,
|
|
984
|
+
isAnswered,
|
|
985
|
+
canSubmit,
|
|
986
|
+
submitting,
|
|
987
|
+
error,
|
|
988
|
+
submit
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
748
992
|
//#endregion
|
|
749
993
|
//#region src/icons/index.tsx
|
|
750
994
|
const CheckIcon = () => {
|
|
@@ -942,7 +1186,7 @@ function SunboardTour() {
|
|
|
942
1186
|
return;
|
|
943
1187
|
}
|
|
944
1188
|
reportTourError(null);
|
|
945
|
-
dismissTour();
|
|
1189
|
+
dismissTour({ silent: true });
|
|
946
1190
|
}, [
|
|
947
1191
|
activeTour,
|
|
948
1192
|
dismissTour,
|
|
@@ -1231,12 +1475,130 @@ function SunboardChecklist() {
|
|
|
1231
1475
|
]
|
|
1232
1476
|
});
|
|
1233
1477
|
}
|
|
1478
|
+
function SunboardSurvey() {
|
|
1479
|
+
const { visible, title, subtitle, currentStep, stepIndex, totalSteps, progress, isFirstStep, isLastStep, canAdvance, canSubmit, next, back, answers, setAnswer, toggleAnswer, submitting, error } = useSurvey();
|
|
1480
|
+
if (!visible || !title || !currentStep) return null;
|
|
1481
|
+
const showProgress = totalSteps > 1;
|
|
1482
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1483
|
+
className: "sunboard-survey-overlay",
|
|
1484
|
+
children: /* @__PURE__ */ jsxs("section", {
|
|
1485
|
+
className: "sunboard-survey-card",
|
|
1486
|
+
role: "dialog",
|
|
1487
|
+
"aria-modal": "true",
|
|
1488
|
+
"aria-label": `${title} survey`,
|
|
1489
|
+
children: [
|
|
1490
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1491
|
+
className: "sunboard-survey-head",
|
|
1492
|
+
children: [subtitle ? /* @__PURE__ */ jsx("p", {
|
|
1493
|
+
className: "sunboard-eyebrow",
|
|
1494
|
+
children: subtitle
|
|
1495
|
+
}) : null, /* @__PURE__ */ jsx("h2", {
|
|
1496
|
+
className: "sunboard-title",
|
|
1497
|
+
children: title
|
|
1498
|
+
})]
|
|
1499
|
+
}),
|
|
1500
|
+
showProgress ? /* @__PURE__ */ jsxs("div", {
|
|
1501
|
+
className: "sunboard-survey-progress-row",
|
|
1502
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
1503
|
+
className: "sunboard-survey-stepcount",
|
|
1504
|
+
children: [
|
|
1505
|
+
"Step ",
|
|
1506
|
+
stepIndex + 1,
|
|
1507
|
+
" of ",
|
|
1508
|
+
totalSteps
|
|
1509
|
+
]
|
|
1510
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1511
|
+
className: "sunboard-progress",
|
|
1512
|
+
role: "progressbar",
|
|
1513
|
+
"aria-label": `${title} progress`,
|
|
1514
|
+
"aria-valuemin": 1,
|
|
1515
|
+
"aria-valuemax": totalSteps,
|
|
1516
|
+
"aria-valuenow": stepIndex + 1,
|
|
1517
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1518
|
+
className: "sunboard-progress-fill",
|
|
1519
|
+
style: { width: `${progress}%` }
|
|
1520
|
+
})
|
|
1521
|
+
})]
|
|
1522
|
+
}) : null,
|
|
1523
|
+
/* @__PURE__ */ jsxs("form", {
|
|
1524
|
+
className: "sunboard-survey-form",
|
|
1525
|
+
onSubmit: (event) => {
|
|
1526
|
+
event.preventDefault();
|
|
1527
|
+
next();
|
|
1528
|
+
},
|
|
1529
|
+
children: [
|
|
1530
|
+
currentStep.title ? /* @__PURE__ */ jsx("p", {
|
|
1531
|
+
className: "sunboard-survey-step-title",
|
|
1532
|
+
children: currentStep.title
|
|
1533
|
+
}) : null,
|
|
1534
|
+
/* @__PURE__ */ jsx("ol", {
|
|
1535
|
+
className: "sunboard-survey-questions",
|
|
1536
|
+
children: currentStep.questions.map((question) => /* @__PURE__ */ jsxs("li", {
|
|
1537
|
+
className: "sunboard-survey-question",
|
|
1538
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
1539
|
+
className: "sunboard-survey-prompt",
|
|
1540
|
+
children: [question.prompt, question.required ? /* @__PURE__ */ jsx("span", {
|
|
1541
|
+
"aria-hidden": "true",
|
|
1542
|
+
className: "sunboard-survey-required",
|
|
1543
|
+
children: " *"
|
|
1544
|
+
}) : null]
|
|
1545
|
+
}), question.type === "text" ? /* @__PURE__ */ jsx("input", {
|
|
1546
|
+
className: "sunboard-survey-input",
|
|
1547
|
+
type: "text",
|
|
1548
|
+
value: typeof answers[question.key] === "string" ? answers[question.key] : "",
|
|
1549
|
+
required: question.required,
|
|
1550
|
+
"aria-label": question.prompt,
|
|
1551
|
+
onChange: (event) => setAnswer(question.key, event.target.value)
|
|
1552
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1553
|
+
className: "sunboard-survey-options",
|
|
1554
|
+
role: "group",
|
|
1555
|
+
"aria-label": question.prompt,
|
|
1556
|
+
children: question.options.map((option) => {
|
|
1557
|
+
const selected = question.type === "multi-select" ? Array.isArray(answers[question.key]) && answers[question.key].includes(option.value) : answers[question.key] === option.value;
|
|
1558
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1559
|
+
className: "sunboard-survey-option",
|
|
1560
|
+
type: "button",
|
|
1561
|
+
"aria-pressed": selected,
|
|
1562
|
+
"data-selected": selected ? "true" : "false",
|
|
1563
|
+
onClick: () => question.type === "multi-select" ? toggleAnswer(question.key, option.value) : setAnswer(question.key, option.value),
|
|
1564
|
+
children: option.label
|
|
1565
|
+
}, option.value);
|
|
1566
|
+
})
|
|
1567
|
+
})]
|
|
1568
|
+
}, question.key))
|
|
1569
|
+
}),
|
|
1570
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
1571
|
+
className: "sunboard-survey-error",
|
|
1572
|
+
role: "alert",
|
|
1573
|
+
children: error
|
|
1574
|
+
}) : null,
|
|
1575
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1576
|
+
className: "sunboard-survey-nav",
|
|
1577
|
+
children: [!isFirstStep ? /* @__PURE__ */ jsx("button", {
|
|
1578
|
+
className: "sunboard-survey-back",
|
|
1579
|
+
type: "button",
|
|
1580
|
+
onClick: back,
|
|
1581
|
+
children: "Back"
|
|
1582
|
+
}) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsx("button", {
|
|
1583
|
+
className: "sunboard-survey-submit",
|
|
1584
|
+
type: "submit",
|
|
1585
|
+
disabled: isLastStep ? !canSubmit : !canAdvance,
|
|
1586
|
+
children: isLastStep ? submitting ? "Submitting…" : "Finish" : "Continue"
|
|
1587
|
+
})]
|
|
1588
|
+
})
|
|
1589
|
+
]
|
|
1590
|
+
})
|
|
1591
|
+
]
|
|
1592
|
+
})
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1234
1595
|
const Sunboard = Object.assign(SunboardChecklist, {
|
|
1235
1596
|
Checklist: SunboardChecklist,
|
|
1597
|
+
Survey: SunboardSurvey,
|
|
1236
1598
|
Hotspots: SunboardHotspots,
|
|
1237
1599
|
Tour: SunboardTour
|
|
1238
1600
|
});
|
|
1239
1601
|
|
|
1240
1602
|
//#endregion
|
|
1241
|
-
export { Sunboard, SunboardChecklist, SunboardHotspots, SunboardProvider, SunboardRuntimeApiError, SunboardRuntimeClient, SunboardTour, useChecklist, useSunboard, useSunboardPreviewSpec };
|
|
1603
|
+
export { Sunboard, SunboardChecklist, SunboardHotspots, SunboardProvider, SunboardRuntimeApiError, SunboardRuntimeClient, SunboardSurvey, SunboardTour, useChecklist, useSunboard, useSunboardPreviewSpec, useSurvey };
|
|
1242
1604
|
//# sourceMappingURL=index.mjs.map
|