@opengovsg/confetti 0.0.0 → 0.0.2
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/README.md +94 -0
- package/build/confetti.css +51 -8
- package/build/index.d.ts +103 -33
- package/build/index.js +260 -141
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @opengovsg/confetti
|
|
2
|
+
|
|
3
|
+
Embeddable React survey widget for collecting user feedback.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@opengovsg/confetti)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# npm
|
|
11
|
+
npm install @opengovsg/confetti
|
|
12
|
+
|
|
13
|
+
# yarn
|
|
14
|
+
yarn add @opengovsg/confetti
|
|
15
|
+
|
|
16
|
+
# pnpm
|
|
17
|
+
pnpm add @opengovsg/confetti
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Peer Dependencies
|
|
21
|
+
|
|
22
|
+
This package requires React 18.2.0+ or React 19:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install react@^18.2.0
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
1. Import the CSS file in your application entry point:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import '@opengovsg/confetti/confetti.css'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. Add the widget to your page:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { PopoverConfetti } from '@opengovsg/confetti'
|
|
40
|
+
|
|
41
|
+
function App() {
|
|
42
|
+
return (
|
|
43
|
+
<div style={{ position: 'fixed', bottom: '1rem', right: '1rem' }}>
|
|
44
|
+
<PopoverConfetti
|
|
45
|
+
surveyId="your-survey-id"
|
|
46
|
+
publishableKey="your-publishable-key"
|
|
47
|
+
metadata={{}}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **Note:** You'll need the survey ID and publishable key from your Confetti dashboard. Make sure to whitelist your domain in your team settings.
|
|
55
|
+
|
|
56
|
+
3. Add Confetti to your CSP headers:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
connect-src https://confetti.hack2026.gov.sg;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Template Components
|
|
63
|
+
|
|
64
|
+
Choose the component that best fits your use case:
|
|
65
|
+
|
|
66
|
+
| Component | Use Case | Behavior |
|
|
67
|
+
| ------------------ | --------------------------------- | --------------------------------------------------------- |
|
|
68
|
+
| `EmbeddedConfetti` | Inline surveys, feedback sections | Shows questions in an inline manner with a submit button |
|
|
69
|
+
| `PopoverConfetti` | Floating feedback widgets | Dismissible popover, step-by-step questions, auto-submits |
|
|
70
|
+
| `StepperConfetti` | Guided surveys | One question at a time, auto-submits after last question |
|
|
71
|
+
|
|
72
|
+
## Visibility Hooks
|
|
73
|
+
|
|
74
|
+
Control when your survey appears with these hooks:
|
|
75
|
+
|
|
76
|
+
### useVisibleAfterDelay
|
|
77
|
+
|
|
78
|
+
Show the survey after a delay:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { useVisibleAfterDelay } from '@opengovsg/confetti'
|
|
82
|
+
|
|
83
|
+
const { isVisible } = useVisibleAfterDelay({ delay: 5000 }) // 5 seconds
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### useVisibleAfterScroll
|
|
87
|
+
|
|
88
|
+
Show the survey after scrolling a certain distance:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { useVisibleAfterScroll } from '@opengovsg/confetti'
|
|
92
|
+
|
|
93
|
+
const { isVisible } = useVisibleAfterScroll({ threshold: 500 }) // 500px
|
|
94
|
+
```
|
package/build/confetti.css
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
--confetti-border-bottom: 1.5px solid var(--confetti-border-color);
|
|
11
11
|
--confetti-border-radius: 12px;
|
|
12
12
|
--confetti-background-color: #ffffff;
|
|
13
|
+
--confetti-hover-background-color: #f5f5f5;
|
|
13
14
|
--confetti-box-shadow:
|
|
14
15
|
0 4px 6px -4px rgba(16, 24, 40, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
15
16
|
--confetti-focus-shadow: 0 0 0 2px rgba(9, 9, 11, 0.3);
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
.confetti-dialog {
|
|
42
|
+
max-width: var(--confetti-max-width);
|
|
41
43
|
background: transparent;
|
|
42
44
|
position: relative;
|
|
43
45
|
|
|
@@ -87,7 +89,6 @@
|
|
|
87
89
|
color: var(--confetti-text-primary-color);
|
|
88
90
|
font-size: 16px;
|
|
89
91
|
width: 100%;
|
|
90
|
-
max-width: var(--confetti-max-width);
|
|
91
92
|
background: var(--confetti-background-color);
|
|
92
93
|
|
|
93
94
|
box-shadow: var(--confetti-box-shadow);
|
|
@@ -104,9 +105,7 @@
|
|
|
104
105
|
font-weight: 500;
|
|
105
106
|
line-height: 20px;
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
transform: scale(1.05);
|
|
109
|
-
}
|
|
108
|
+
transition: background 200ms ease-out;
|
|
110
109
|
|
|
111
110
|
&:focus-visible {
|
|
112
111
|
outline: 2px solid var(--confetti-outline-color);
|
|
@@ -118,6 +117,22 @@
|
|
|
118
117
|
cursor: not-allowed;
|
|
119
118
|
transform: none;
|
|
120
119
|
}
|
|
120
|
+
|
|
121
|
+
&:hover {
|
|
122
|
+
background: color-mix(
|
|
123
|
+
in srgb,
|
|
124
|
+
white 10%,
|
|
125
|
+
var(--confetti-submit-button-color)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&:active {
|
|
130
|
+
background: color-mix(
|
|
131
|
+
in srgb,
|
|
132
|
+
white 5%,
|
|
133
|
+
var(--confetti-submit-button-color)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
.sr-only {
|
|
@@ -138,13 +153,40 @@
|
|
|
138
153
|
gap: 12px;
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
.cf-choice {
|
|
157
|
+
font-size: 16px;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
padding: 8px 10px;
|
|
160
|
+
border-radius: 6px;
|
|
161
|
+
border: 1px solid var(--confetti-border-color);
|
|
162
|
+
|
|
163
|
+
transition: background 200ms ease-out;
|
|
164
|
+
|
|
165
|
+
&:hover {
|
|
166
|
+
background: var(--confetti-hover-background-color);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.cf-choice-label {
|
|
170
|
+
gap: 8px;
|
|
171
|
+
display: inline-flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.cf-choice-description {
|
|
176
|
+
font-size: 0.875em;
|
|
177
|
+
margin-left: 26px;
|
|
178
|
+
color: var(--confetti-text-subtle-color);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
.cf-radio,
|
|
142
183
|
.cf-checkbox {
|
|
184
|
+
cursor: pointer;
|
|
143
185
|
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
144
187
|
/* This is needed so the hidden input is positioned correctly */
|
|
145
188
|
position: relative;
|
|
146
|
-
align-items:
|
|
147
|
-
gap: 8px;
|
|
189
|
+
align-items: start;
|
|
148
190
|
color: var(--confetti-text-primary-color);
|
|
149
191
|
forced-color-adjust: none;
|
|
150
192
|
}
|
|
@@ -153,7 +195,7 @@
|
|
|
153
195
|
--checkmark-color: var(--confetti-indicator-active-dot-color);
|
|
154
196
|
|
|
155
197
|
.cf-checkbox-indicator {
|
|
156
|
-
background:
|
|
198
|
+
background: white;
|
|
157
199
|
border: 1px solid var(--confetti-border-color);
|
|
158
200
|
width: 18px;
|
|
159
201
|
height: 18px;
|
|
@@ -217,6 +259,7 @@
|
|
|
217
259
|
border-radius: 50%;
|
|
218
260
|
transition: color 200ms ease-out;
|
|
219
261
|
border: 1px solid var(--confetti-border-color);
|
|
262
|
+
background: white;
|
|
220
263
|
|
|
221
264
|
&::after {
|
|
222
265
|
content: '';
|
|
@@ -303,7 +346,7 @@
|
|
|
303
346
|
.cf-others-choice {
|
|
304
347
|
display: flex;
|
|
305
348
|
flex-direction: column;
|
|
306
|
-
gap:
|
|
349
|
+
gap: 8px;
|
|
307
350
|
}
|
|
308
351
|
}
|
|
309
352
|
|
package/build/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as react0 from "react";
|
|
2
2
|
import { HTMLAttributes, PropsWithChildren } from "react";
|
|
3
3
|
import { z } from "zod/v4/mini";
|
|
4
|
-
import * as
|
|
4
|
+
import * as react_jsx_runtime3 from "react/jsx-runtime";
|
|
5
5
|
|
|
6
6
|
//#region ../validators/src/questions/properties.d.ts
|
|
7
|
+
|
|
7
8
|
declare const ratingQuestionSchema: z.ZodMiniObject<{
|
|
8
9
|
type: z.ZodMiniLiteral<"rating">;
|
|
9
10
|
scale: z.ZodMiniNumber<number>;
|
|
@@ -22,24 +23,26 @@ type TextQuestion = z.infer<typeof textQuestionSchema>;
|
|
|
22
23
|
declare const singleSelectQuestionSchema: z.ZodMiniObject<{
|
|
23
24
|
type: z.ZodMiniLiteral<"single-select">;
|
|
24
25
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
25
|
-
label: z.ZodMiniString<string>;
|
|
26
26
|
value: z.ZodMiniString<string>;
|
|
27
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
27
28
|
}, z.core.$strip>>;
|
|
28
29
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
29
30
|
label: z.ZodMiniString<string>;
|
|
30
31
|
max: z.ZodMiniNumber<number>;
|
|
32
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
31
33
|
}, z.core.$strip>>;
|
|
32
34
|
}, z.core.$strip>;
|
|
33
35
|
type SingleSelectQuestion = z.infer<typeof singleSelectQuestionSchema>;
|
|
34
36
|
declare const multiSelectQuestionSchema: z.ZodMiniObject<{
|
|
35
37
|
type: z.ZodMiniLiteral<"multi-select">;
|
|
36
38
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
37
|
-
label: z.ZodMiniString<string>;
|
|
38
39
|
value: z.ZodMiniString<string>;
|
|
40
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
39
41
|
}, z.core.$strip>>;
|
|
40
42
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
41
43
|
label: z.ZodMiniString<string>;
|
|
42
44
|
max: z.ZodMiniNumber<number>;
|
|
45
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
43
46
|
}, z.core.$strip>>;
|
|
44
47
|
max: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
45
48
|
}, z.core.$strip>;
|
|
@@ -66,12 +69,13 @@ declare const publicGetSurveyOutputSchema: z.ZodMiniObject<{
|
|
|
66
69
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
67
70
|
type: z.ZodMiniLiteral<"single-select">;
|
|
68
71
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
69
|
-
label: z.ZodMiniString<string>;
|
|
70
72
|
value: z.ZodMiniString<string>;
|
|
73
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
71
74
|
}, z.core.$strip>>;
|
|
72
75
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
73
76
|
label: z.ZodMiniString<string>;
|
|
74
77
|
max: z.ZodMiniNumber<number>;
|
|
78
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
75
79
|
}, z.core.$strip>>;
|
|
76
80
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
77
81
|
type: z.ZodMiniLiteral<"open-ended">;
|
|
@@ -79,12 +83,13 @@ declare const publicGetSurveyOutputSchema: z.ZodMiniObject<{
|
|
|
79
83
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
80
84
|
type: z.ZodMiniLiteral<"multi-select">;
|
|
81
85
|
options: z.ZodMiniArray<z.ZodMiniObject<{
|
|
82
|
-
label: z.ZodMiniString<string>;
|
|
83
86
|
value: z.ZodMiniString<string>;
|
|
87
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
84
88
|
}, z.core.$strip>>;
|
|
85
89
|
editable: z.ZodMiniNullable<z.ZodMiniObject<{
|
|
86
90
|
label: z.ZodMiniString<string>;
|
|
87
91
|
max: z.ZodMiniNumber<number>;
|
|
92
|
+
description: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
88
93
|
}, z.core.$strip>>;
|
|
89
94
|
max: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
90
95
|
}, z.core.$strip>], "type">;
|
|
@@ -126,17 +131,22 @@ declare const publicGetSurveyOutputSchema: z.ZodMiniObject<{
|
|
|
126
131
|
}, z.core.$strip>], "type">;
|
|
127
132
|
}, z.core.$strip>], "type">;
|
|
128
133
|
}, z.core.$strip>>;
|
|
134
|
+
confirmation: z.ZodMiniObject<{
|
|
135
|
+
title: z.ZodMiniString<string>;
|
|
136
|
+
description: z.ZodMiniString<string>;
|
|
137
|
+
buttonText: z.ZodMiniString<string>;
|
|
138
|
+
}, z.core.$strip>;
|
|
129
139
|
}, z.core.$strip>;
|
|
130
140
|
type PublicGetSurveyOutput = z.infer<typeof publicGetSurveyOutputSchema>;
|
|
131
141
|
//#endregion
|
|
132
|
-
//#region src/types
|
|
133
|
-
type Question
|
|
142
|
+
//#region src/core/types.d.ts
|
|
143
|
+
type Question = {
|
|
134
144
|
visible: boolean;
|
|
135
145
|
error?: string;
|
|
136
146
|
} & PublicGetSurveyOutput['questions'][number];
|
|
137
147
|
type Answer = string | number | string[] | undefined;
|
|
138
148
|
interface ConfettiState {
|
|
139
|
-
questions: Question
|
|
149
|
+
questions: Question[];
|
|
140
150
|
current: number;
|
|
141
151
|
update: ({
|
|
142
152
|
question,
|
|
@@ -151,10 +161,10 @@ interface ConfettiState {
|
|
|
151
161
|
submit: () => void;
|
|
152
162
|
isCompleted: boolean;
|
|
153
163
|
isSubmitted: boolean;
|
|
154
|
-
errors: Record<Question
|
|
164
|
+
errors: Record<Question['id'], string>;
|
|
155
165
|
}
|
|
156
166
|
//#endregion
|
|
157
|
-
//#region src/
|
|
167
|
+
//#region src/core/ConfettiController.d.ts
|
|
158
168
|
interface ConfettiControllerProps {
|
|
159
169
|
children: (props: ConfettiState) => React.ReactElement;
|
|
160
170
|
/**
|
|
@@ -170,16 +180,16 @@ declare const ConfettiController: ({
|
|
|
170
180
|
shouldAutoSubmit,
|
|
171
181
|
onFinished,
|
|
172
182
|
children
|
|
173
|
-
}: ConfettiControllerProps) =>
|
|
183
|
+
}: ConfettiControllerProps) => react_jsx_runtime3.JSX.Element;
|
|
174
184
|
//#endregion
|
|
175
|
-
//#region src/
|
|
185
|
+
//#region src/core/ConfettiProvider.d.ts
|
|
176
186
|
interface ConfettiContextValue {
|
|
177
187
|
surveyId: string;
|
|
178
188
|
metadata: Record<string, string>;
|
|
179
189
|
submit: ({
|
|
180
190
|
answers
|
|
181
191
|
}: {
|
|
182
|
-
answers: Record<Question
|
|
192
|
+
answers: Record<Question['id'], Answer>;
|
|
183
193
|
}) => Promise<void>;
|
|
184
194
|
dismiss: () => Promise<void> | void;
|
|
185
195
|
survey: PublicGetSurveyOutput | undefined;
|
|
@@ -190,7 +200,7 @@ interface ConfettiProviderProps {
|
|
|
190
200
|
surveyId: string;
|
|
191
201
|
publishableKey: string;
|
|
192
202
|
respondent?: string;
|
|
193
|
-
metadata
|
|
203
|
+
metadata?: Record<string, string>;
|
|
194
204
|
apiBaseUrl?: string;
|
|
195
205
|
isSurveyVisible?: boolean | ((_: {
|
|
196
206
|
lastRespondedAt: Date | null;
|
|
@@ -204,16 +214,17 @@ declare const ConfettiProvider: ({
|
|
|
204
214
|
metadata,
|
|
205
215
|
isSurveyVisible: isSurveyVisibleInput,
|
|
206
216
|
apiBaseUrl
|
|
207
|
-
}: ConfettiProviderProps) =>
|
|
217
|
+
}: ConfettiProviderProps) => react_jsx_runtime3.JSX.Element | null;
|
|
208
218
|
declare const useConfetti: () => ConfettiContextValue;
|
|
209
219
|
//#endregion
|
|
210
220
|
//#region src/components/Confetti.d.ts
|
|
211
221
|
interface ConfettiProps extends Omit<ConfettiProviderProps, 'children'>, Pick<ConfettiControllerProps, 'onFinished' | 'shouldAutoSubmit'> {}
|
|
212
222
|
declare const Confetti: ({
|
|
213
223
|
children,
|
|
224
|
+
onFinished,
|
|
214
225
|
shouldAutoSubmit,
|
|
215
226
|
...props
|
|
216
|
-
}: ConfettiProps & Pick<ConfettiControllerProps, "children" | "shouldAutoSubmit">) =>
|
|
227
|
+
}: ConfettiProps & Pick<ConfettiControllerProps, "children" | "shouldAutoSubmit">) => react_jsx_runtime3.JSX.Element;
|
|
217
228
|
//#endregion
|
|
218
229
|
//#region src/components/ThankYouMessage.d.ts
|
|
219
230
|
interface ThankYouMessageProps {
|
|
@@ -230,11 +241,11 @@ declare const ThankYouMessage: ({
|
|
|
230
241
|
title,
|
|
231
242
|
description,
|
|
232
243
|
classNames
|
|
233
|
-
}: ThankYouMessageProps) =>
|
|
244
|
+
}: ThankYouMessageProps) => react_jsx_runtime3.JSX.Element | null;
|
|
234
245
|
//#endregion
|
|
235
246
|
//#region src/components/container/PopoverContainer.d.ts
|
|
236
247
|
interface PopoverContainerProps {
|
|
237
|
-
closeOnFinishMs?: number;
|
|
248
|
+
closeOnFinishMs?: number | null;
|
|
238
249
|
classNames?: {
|
|
239
250
|
dialog?: string;
|
|
240
251
|
closeButton?: string;
|
|
@@ -252,10 +263,10 @@ declare const PopoverContainer: ({
|
|
|
252
263
|
containerProps,
|
|
253
264
|
onDismiss,
|
|
254
265
|
closeOnFinishMs
|
|
255
|
-
}: PopoverContainerProps) =>
|
|
266
|
+
}: PopoverContainerProps) => react_jsx_runtime3.JSX.Element;
|
|
256
267
|
//#endregion
|
|
257
268
|
//#region src/components/questions/types.d.ts
|
|
258
|
-
type Question = PublicGetSurveyOutput['questions'][number];
|
|
269
|
+
type Question$1 = PublicGetSurveyOutput['questions'][number];
|
|
259
270
|
interface BaseFieldProps {
|
|
260
271
|
id: string;
|
|
261
272
|
title: string | null | undefined;
|
|
@@ -267,7 +278,7 @@ interface BaseFieldProps {
|
|
|
267
278
|
//#endregion
|
|
268
279
|
//#region src/components/questions/factory.d.ts
|
|
269
280
|
interface SurveyQuestionFactoryProps {
|
|
270
|
-
question: Question;
|
|
281
|
+
question: Question$1;
|
|
271
282
|
onChange: (answer: Answer) => void;
|
|
272
283
|
error?: string;
|
|
273
284
|
}
|
|
@@ -275,7 +286,7 @@ declare const SurveyQuestionFactory: ({
|
|
|
275
286
|
question,
|
|
276
287
|
onChange,
|
|
277
288
|
error
|
|
278
|
-
}: SurveyQuestionFactoryProps) =>
|
|
289
|
+
}: SurveyQuestionFactoryProps) => react_jsx_runtime3.JSX.Element;
|
|
279
290
|
//#endregion
|
|
280
291
|
//#region src/components/questions/Rating.d.ts
|
|
281
292
|
interface RatingProps extends BaseFieldProps {
|
|
@@ -308,7 +319,7 @@ declare const Rating: ({
|
|
|
308
319
|
id,
|
|
309
320
|
error,
|
|
310
321
|
required
|
|
311
|
-
}: RatingProps) =>
|
|
322
|
+
}: RatingProps) => react_jsx_runtime3.JSX.Element;
|
|
312
323
|
//#endregion
|
|
313
324
|
//#region src/components/questions/FreeText.d.ts
|
|
314
325
|
interface FreeTextProps extends BaseFieldProps {
|
|
@@ -331,7 +342,7 @@ declare const FreeText: ({
|
|
|
331
342
|
classNames,
|
|
332
343
|
required,
|
|
333
344
|
error
|
|
334
|
-
}: FreeTextProps) =>
|
|
345
|
+
}: FreeTextProps) => react_jsx_runtime3.JSX.Element;
|
|
335
346
|
//#endregion
|
|
336
347
|
//#region src/components/questions/SingleChoice.d.ts
|
|
337
348
|
interface SingleChoiceProps extends BaseFieldProps {
|
|
@@ -339,6 +350,7 @@ interface SingleChoiceProps extends BaseFieldProps {
|
|
|
339
350
|
classNames?: {
|
|
340
351
|
question?: string;
|
|
341
352
|
header?: string;
|
|
353
|
+
label?: string;
|
|
342
354
|
title?: string;
|
|
343
355
|
description?: string;
|
|
344
356
|
group?: string;
|
|
@@ -346,6 +358,7 @@ interface SingleChoiceProps extends BaseFieldProps {
|
|
|
346
358
|
indicator?: string;
|
|
347
359
|
othersChoice?: string;
|
|
348
360
|
othersInput?: string;
|
|
361
|
+
choiceDescription?: string;
|
|
349
362
|
error?: string;
|
|
350
363
|
};
|
|
351
364
|
}
|
|
@@ -358,7 +371,7 @@ declare const SingleChoice: ({
|
|
|
358
371
|
id,
|
|
359
372
|
required,
|
|
360
373
|
error
|
|
361
|
-
}: SingleChoiceProps) =>
|
|
374
|
+
}: SingleChoiceProps) => react_jsx_runtime3.JSX.Element;
|
|
362
375
|
//#endregion
|
|
363
376
|
//#region src/components/questions/MultiChoice.d.ts
|
|
364
377
|
interface MultiChoiceProps extends BaseFieldProps {
|
|
@@ -366,8 +379,10 @@ interface MultiChoiceProps extends BaseFieldProps {
|
|
|
366
379
|
classNames?: {
|
|
367
380
|
question?: string;
|
|
368
381
|
header?: string;
|
|
382
|
+
label?: string;
|
|
369
383
|
title?: string;
|
|
370
384
|
description?: string;
|
|
385
|
+
choiceDescription?: string;
|
|
371
386
|
group?: string;
|
|
372
387
|
checkbox?: string;
|
|
373
388
|
indicator?: string;
|
|
@@ -386,26 +401,26 @@ declare const MultiChoice: ({
|
|
|
386
401
|
id,
|
|
387
402
|
required,
|
|
388
403
|
error
|
|
389
|
-
}: MultiChoiceProps) =>
|
|
404
|
+
}: MultiChoiceProps) => react_jsx_runtime3.JSX.Element;
|
|
390
405
|
//#endregion
|
|
391
406
|
//#region src/components/presentation/Flow.d.ts
|
|
392
407
|
type ChildField = ({
|
|
393
408
|
onChange
|
|
394
409
|
}: {
|
|
395
410
|
onChange: (answer: Answer) => void;
|
|
396
|
-
question: Question;
|
|
411
|
+
question: Question$1;
|
|
397
412
|
error?: string;
|
|
398
413
|
}) => React.ReactNode;
|
|
399
414
|
interface FlowProps {
|
|
400
415
|
onSubmit: (answer: Answer) => void;
|
|
401
416
|
children: ChildField;
|
|
402
|
-
question: Question;
|
|
417
|
+
question: Question$1;
|
|
403
418
|
}
|
|
404
419
|
declare const Flow: ({
|
|
405
420
|
onSubmit,
|
|
406
421
|
children,
|
|
407
422
|
question
|
|
408
|
-
}: FlowProps) =>
|
|
423
|
+
}: FlowProps) => react_jsx_runtime3.JSX.Element;
|
|
409
424
|
//#endregion
|
|
410
425
|
//#region src/components/presentation/Embed.d.ts
|
|
411
426
|
declare const Embed: ({
|
|
@@ -415,13 +430,68 @@ declare const Embed: ({
|
|
|
415
430
|
}: PropsWithChildren<{
|
|
416
431
|
isCompleted: boolean;
|
|
417
432
|
onSubmit: () => void;
|
|
418
|
-
}>) =>
|
|
433
|
+
}>) => react_jsx_runtime3.JSX.Element;
|
|
419
434
|
//#endregion
|
|
420
435
|
//#region src/templates/EmbeddedConfetti.d.ts
|
|
421
|
-
declare const EmbeddedConfetti: (props: ConfettiProps) =>
|
|
436
|
+
declare const EmbeddedConfetti: (props: ConfettiProps) => react_jsx_runtime3.JSX.Element;
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/templates/PopoverConfetti.d.ts
|
|
439
|
+
interface PopoverConfettiProps extends ConfettiProps {
|
|
440
|
+
/**
|
|
441
|
+
* The popover will automatically close this many milliseconds
|
|
442
|
+
* after the survey is finished.
|
|
443
|
+
* Set to `null` to disable automatic closing.
|
|
444
|
+
* @default 3000
|
|
445
|
+
*/
|
|
446
|
+
closeOnFinishMs?: number | null;
|
|
447
|
+
}
|
|
448
|
+
declare const PopoverConfetti: ({
|
|
449
|
+
closeOnFinishMs,
|
|
450
|
+
...props
|
|
451
|
+
}: PopoverConfettiProps) => react_jsx_runtime3.JSX.Element;
|
|
422
452
|
//#endregion
|
|
423
453
|
//#region src/templates/StepperConfetti.d.ts
|
|
424
|
-
declare const StepperConfetti: (props: ConfettiProps) =>
|
|
454
|
+
declare const StepperConfetti: (props: ConfettiProps) => react_jsx_runtime3.JSX.Element;
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/hooks/useVisibleAfterDelay.d.ts
|
|
457
|
+
interface UseVisibleAfterDelayOptions {
|
|
458
|
+
delay: number;
|
|
459
|
+
}
|
|
460
|
+
declare const useVisibleAfterDelay: ({
|
|
461
|
+
delay
|
|
462
|
+
}: UseVisibleAfterDelayOptions) => {
|
|
463
|
+
isVisible: boolean;
|
|
464
|
+
};
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/hooks/useVisibleAfterPageVisits.d.ts
|
|
467
|
+
interface UseVisibleAfterPageVisitsOptions {
|
|
468
|
+
visits: number;
|
|
469
|
+
respondent?: string;
|
|
470
|
+
}
|
|
471
|
+
declare const useVisibleAfterPageVisits: ({
|
|
472
|
+
visits,
|
|
473
|
+
respondent
|
|
474
|
+
}: UseVisibleAfterPageVisitsOptions) => {
|
|
475
|
+
isVisible: boolean;
|
|
476
|
+
reset: () => void;
|
|
477
|
+
};
|
|
478
|
+
declare const useVisibleAfterSessionPageVisits: ({
|
|
479
|
+
visits,
|
|
480
|
+
respondent
|
|
481
|
+
}: UseVisibleAfterPageVisitsOptions) => {
|
|
482
|
+
isVisible: boolean;
|
|
483
|
+
reset: () => void;
|
|
484
|
+
};
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/hooks/useVisibleAfterScroll.d.ts
|
|
487
|
+
interface UseVisibleAfterScrollOptions {
|
|
488
|
+
threshold: number;
|
|
489
|
+
}
|
|
490
|
+
declare const useVisibleAfterScroll: ({
|
|
491
|
+
threshold
|
|
492
|
+
}: UseVisibleAfterScrollOptions) => {
|
|
493
|
+
isVisible: boolean;
|
|
494
|
+
};
|
|
425
495
|
//#endregion
|
|
426
|
-
export { Confetti, ConfettiContext, ConfettiController, type ConfettiControllerProps, type ConfettiProps, ConfettiProvider, type ConfettiProviderProps, Embed, EmbeddedConfetti, Flow, FreeText, type FreeTextProps, MultiChoice, type MultiChoiceProps, PopoverContainer, type PopoverContainerProps, Rating, type RatingProps, SingleChoice, type SingleChoiceProps, StepperConfetti, SurveyQuestionFactory, type SurveyQuestionFactoryProps, ThankYouMessage, useConfetti };
|
|
496
|
+
export { type Answer, Confetti, ConfettiContext, ConfettiController, type ConfettiControllerProps, type ConfettiProps, ConfettiProvider, type ConfettiProviderProps, type ConfettiState, Embed, EmbeddedConfetti, Flow, FreeText, type FreeTextProps, MultiChoice, type MultiChoiceProps, PopoverConfetti, PopoverContainer, type PopoverContainerProps, type Question, Rating, type RatingProps, SingleChoice, type SingleChoiceProps, StepperConfetti, SurveyQuestionFactory, type SurveyQuestionFactoryProps, ThankYouMessage, useConfetti, useVisibleAfterDelay, useVisibleAfterPageVisits, useVisibleAfterScroll, useVisibleAfterSessionPageVisits };
|
|
427
497
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.js
CHANGED
|
@@ -1,67 +1,16 @@
|
|
|
1
1
|
import { ErrorBoundary } from "react-error-boundary";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useLocalStorage, useSessionStorage, useTimeout } from "usehooks-ts";
|
|
3
4
|
import { z } from "zod/v4/mini";
|
|
4
|
-
import { useLocalStorage, useTimeout } from "usehooks-ts";
|
|
5
5
|
import ky from "ky";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import clsx from "clsx";
|
|
8
8
|
import { Button, Checkbox, CheckboxGroup, FieldError, Input, Label, Radio, RadioGroup, Text, TextArea, TextField, composeRenderProps } from "react-aria-components";
|
|
9
9
|
|
|
10
|
-
//#region src/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const question = questions[i];
|
|
15
|
-
const answer = question ? answers[question.id] : void 0;
|
|
16
|
-
if (!question || !question.visible) continue;
|
|
17
|
-
const logic = question.routingLogic;
|
|
18
|
-
if (logic.type === "always") if (evaluateTarget({
|
|
19
|
-
position: i,
|
|
20
|
-
value: logic.target
|
|
21
|
-
}) === index) return true;
|
|
22
|
-
else continue;
|
|
23
|
-
if ((answer === void 0 || answer === null) && question.required) continue;
|
|
24
|
-
const cases = logic.cases;
|
|
25
|
-
const match = answer !== null ? cases.find((condition) => evaluateCondition({
|
|
26
|
-
answer,
|
|
27
|
-
condition
|
|
28
|
-
})) : void 0;
|
|
29
|
-
if (!match) if (evaluateTarget({
|
|
30
|
-
position: i,
|
|
31
|
-
value: logic.default
|
|
32
|
-
}) === index) return true;
|
|
33
|
-
else continue;
|
|
34
|
-
if (evaluateTarget({
|
|
35
|
-
position: i,
|
|
36
|
-
value: match.target
|
|
37
|
-
}) === index) return true;
|
|
38
|
-
else continue;
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
};
|
|
42
|
-
const evaluateTarget = ({ position, value }) => {
|
|
43
|
-
switch (value.type) {
|
|
44
|
-
case "next": return position + 1;
|
|
45
|
-
case "jump": return value.target;
|
|
46
|
-
case "end": return null;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const evaluateCondition = ({ answer, condition }) => {
|
|
50
|
-
const value = evaluateValue(condition.value);
|
|
51
|
-
switch (condition.operator) {
|
|
52
|
-
case "equal": return answer === value;
|
|
53
|
-
case "not-equal": return answer !== value;
|
|
54
|
-
case "greater": return typeof answer === "number" && typeof value === "number" && answer > value;
|
|
55
|
-
case "greater-or-equal": return typeof answer === "number" && typeof value === "number" && answer >= value;
|
|
56
|
-
case "less": return typeof answer === "number" && typeof value === "number" && answer < value;
|
|
57
|
-
case "less-or-equal": return typeof answer === "number" && typeof value === "number" && answer <= value;
|
|
58
|
-
case "contains": return Array.isArray(answer) && typeof value === "string" && answer.includes(value);
|
|
59
|
-
case "not-contains": return Array.isArray(answer) && typeof value === "string" && !answer.includes(value);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const evaluateValue = (node) => {
|
|
63
|
-
return node.value;
|
|
64
|
-
};
|
|
10
|
+
//#region src/utils.ts
|
|
11
|
+
function prettifyError(error) {
|
|
12
|
+
return error.issues[0]?.message ?? "Invalid input";
|
|
13
|
+
}
|
|
65
14
|
|
|
66
15
|
//#endregion
|
|
67
16
|
//#region ../validators/src/questions/routing.ts
|
|
@@ -109,12 +58,13 @@ const routingLogicSchema = z.discriminatedUnion("type", [alwaysConditionSchema,
|
|
|
109
58
|
//#endregion
|
|
110
59
|
//#region ../validators/src/questions/properties.ts
|
|
111
60
|
const optionSchema = z.object({
|
|
112
|
-
|
|
113
|
-
|
|
61
|
+
value: z.string().check(z.trim(), z.minLength(1, { error: "Value is required" })),
|
|
62
|
+
description: z.optional(z.string().check(z.trim()))
|
|
114
63
|
});
|
|
115
64
|
const editableSchema = z.nullable(z.object({
|
|
116
|
-
label: z.string().check(z.trim(), z.minLength(1, { error: "
|
|
117
|
-
max: z.number()
|
|
65
|
+
label: z.string().check(z.trim(), z.minLength(1, { error: "Value is required" })),
|
|
66
|
+
max: z.number(),
|
|
67
|
+
description: z.optional(z.string().check(z.trim()))
|
|
118
68
|
}));
|
|
119
69
|
const ratingQuestionSchema = z.object({
|
|
120
70
|
type: z.literal("rating"),
|
|
@@ -173,6 +123,11 @@ const multiSelectAnswerSchemaFactory = ({ maxSelected, maxLength }, { required }
|
|
|
173
123
|
|
|
174
124
|
//#endregion
|
|
175
125
|
//#region ../validators/src/survey.ts
|
|
126
|
+
const surveyConfirmationSchema = z.object({
|
|
127
|
+
title: z.string().check(z.trim()),
|
|
128
|
+
description: z.string().check(z.trim()),
|
|
129
|
+
buttonText: z.string().check(z.trim())
|
|
130
|
+
});
|
|
176
131
|
const publicGetSurveyOutputSchema = z.object({
|
|
177
132
|
title: z.string(),
|
|
178
133
|
description: z.nullable(z.string()),
|
|
@@ -184,7 +139,8 @@ const publicGetSurveyOutputSchema = z.object({
|
|
|
184
139
|
required: z.boolean(),
|
|
185
140
|
properties: questionPropertiesSchema,
|
|
186
141
|
routingLogic: routingLogicSchema
|
|
187
|
-
}))
|
|
142
|
+
})),
|
|
143
|
+
confirmation: surveyConfirmationSchema
|
|
188
144
|
});
|
|
189
145
|
|
|
190
146
|
//#endregion
|
|
@@ -201,48 +157,8 @@ const publicPutResponseInputSchema = z.object({
|
|
|
201
157
|
const publicGetRespondentOutputSchema = z.object({ lastRespondedAt: z.nullable(z.union([z.iso.datetime(), z.date()])) });
|
|
202
158
|
|
|
203
159
|
//#endregion
|
|
204
|
-
//#region src/logic/
|
|
205
|
-
const
|
|
206
|
-
rating: ({ scale }, meta) => ratingAnswerSchemaFactory({ scale }, meta),
|
|
207
|
-
"open-ended": ({ max }, meta) => openEndedAnswerSchemaFactory({ maxLength: max }, meta),
|
|
208
|
-
"single-select": ({ editable }, meta) => singleSelectAnswerSchemaFactory({ maxLength: editable?.max }, meta),
|
|
209
|
-
"multi-select": ({ editable, max }, meta) => multiSelectAnswerSchemaFactory({
|
|
210
|
-
maxLength: editable?.max,
|
|
211
|
-
maxSelected: max
|
|
212
|
-
}, meta)
|
|
213
|
-
};
|
|
214
|
-
const getAnswerValidator = (properties, meta) => {
|
|
215
|
-
const type = properties.type;
|
|
216
|
-
switch (type) {
|
|
217
|
-
case "rating": {
|
|
218
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
219
|
-
return validator(properties, meta);
|
|
220
|
-
}
|
|
221
|
-
case "open-ended": {
|
|
222
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
223
|
-
return validator(properties, meta);
|
|
224
|
-
}
|
|
225
|
-
case "single-select": {
|
|
226
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
227
|
-
return validator(properties, meta);
|
|
228
|
-
}
|
|
229
|
-
case "multi-select": {
|
|
230
|
-
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
231
|
-
return validator(properties, meta);
|
|
232
|
-
}
|
|
233
|
-
default: throw new Error("Unsupported question type");
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
//#endregion
|
|
238
|
-
//#region src/utils.ts
|
|
239
|
-
function prettifyError(error) {
|
|
240
|
-
return error.issues[0]?.message ?? "Invalid input";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
//#endregion
|
|
244
|
-
//#region src/logic/api.ts
|
|
245
|
-
const API_HOST = "https://confetti.hack2026.gov.sg/api/cfti";
|
|
160
|
+
//#region src/core/logic/api.ts
|
|
161
|
+
const API_HOST = "https://confetti.hack2026.gov.sg/api";
|
|
246
162
|
const client = ky.create({
|
|
247
163
|
prefixUrl: API_HOST,
|
|
248
164
|
retry: {
|
|
@@ -253,9 +169,9 @@ const client = ky.create({
|
|
|
253
169
|
});
|
|
254
170
|
|
|
255
171
|
//#endregion
|
|
256
|
-
//#region src/
|
|
172
|
+
//#region src/core/ConfettiProvider.tsx
|
|
257
173
|
const ConfettiContext = createContext(null);
|
|
258
|
-
const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, metadata, isSurveyVisible: isSurveyVisibleInput, apiBaseUrl }) => {
|
|
174
|
+
const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, metadata = {}, isSurveyVisible: isSurveyVisibleInput, apiBaseUrl }) => {
|
|
259
175
|
const [survey, setSurvey] = useState();
|
|
260
176
|
const [lastRespondedAt, setLastRespondedAt] = useState();
|
|
261
177
|
const responseId = useMemo(() => crypto.randomUUID(), []);
|
|
@@ -270,13 +186,13 @@ const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, meta
|
|
|
270
186
|
return client.extend(properties);
|
|
271
187
|
}, [apiBaseUrl, publishableKey]);
|
|
272
188
|
useEffect(() => {
|
|
273
|
-
client$1.get(
|
|
189
|
+
client$1.get(`v1/cfti/${surveyId}`).json().then((data) => {
|
|
274
190
|
const parseResult = publicGetSurveyOutputSchema.safeParse(data);
|
|
275
191
|
if (!parseResult.success) return;
|
|
276
192
|
setSurvey(parseResult.data);
|
|
277
193
|
});
|
|
278
194
|
if (!respondent && !internalRespondentId) return;
|
|
279
|
-
client$1.get(
|
|
195
|
+
client$1.get(`v1/cfti/${surveyId}/respondent/${respondent ?? internalRespondentId}`).json().then((data) => {
|
|
280
196
|
const parseResult = publicGetRespondentOutputSchema.safeParse(data);
|
|
281
197
|
if (!parseResult.success) {
|
|
282
198
|
setLastRespondedAt(null);
|
|
@@ -292,7 +208,7 @@ const ConfettiProvider = ({ children, surveyId, publishableKey, respondent, meta
|
|
|
292
208
|
surveyId
|
|
293
209
|
]);
|
|
294
210
|
const submit = useCallback(async ({ answers }) => {
|
|
295
|
-
await client$1.put(
|
|
211
|
+
await client$1.put(`v1/cfti/${surveyId}/response/${responseId}`, { json: {
|
|
296
212
|
respondent: respondent ?? internalRespondentId,
|
|
297
213
|
data: answers,
|
|
298
214
|
metadata
|
|
@@ -332,7 +248,98 @@ const useConfetti = () => {
|
|
|
332
248
|
};
|
|
333
249
|
|
|
334
250
|
//#endregion
|
|
335
|
-
//#region src/
|
|
251
|
+
//#region src/core/logic/route.ts
|
|
252
|
+
const isVisible = ({ index, questions, answers }) => {
|
|
253
|
+
if (index === 0) return true;
|
|
254
|
+
for (let i = 0; i < index; i++) {
|
|
255
|
+
const question = questions[i];
|
|
256
|
+
const answer = question ? answers[question.id] : void 0;
|
|
257
|
+
if (!question || !question.visible) continue;
|
|
258
|
+
const logic = question.routingLogic;
|
|
259
|
+
if (logic.type === "always") if (evaluateTarget({
|
|
260
|
+
position: i,
|
|
261
|
+
value: logic.target
|
|
262
|
+
}) === index) return true;
|
|
263
|
+
else continue;
|
|
264
|
+
if ((answer === void 0 || answer === null) && question.required) continue;
|
|
265
|
+
const cases = logic.cases;
|
|
266
|
+
const match = answer !== null ? cases.find((condition) => evaluateCondition({
|
|
267
|
+
answer,
|
|
268
|
+
condition
|
|
269
|
+
})) : void 0;
|
|
270
|
+
if (!match) if (evaluateTarget({
|
|
271
|
+
position: i,
|
|
272
|
+
value: logic.default
|
|
273
|
+
}) === index) return true;
|
|
274
|
+
else continue;
|
|
275
|
+
if (evaluateTarget({
|
|
276
|
+
position: i,
|
|
277
|
+
value: match.target
|
|
278
|
+
}) === index) return true;
|
|
279
|
+
else continue;
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
};
|
|
283
|
+
const evaluateTarget = ({ position, value }) => {
|
|
284
|
+
switch (value.type) {
|
|
285
|
+
case "next": return position + 1;
|
|
286
|
+
case "jump": return value.target;
|
|
287
|
+
case "end": return null;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const evaluateCondition = ({ answer, condition }) => {
|
|
291
|
+
const value = evaluateValue(condition.value);
|
|
292
|
+
switch (condition.operator) {
|
|
293
|
+
case "equal": return answer === value;
|
|
294
|
+
case "not-equal": return answer !== value;
|
|
295
|
+
case "greater": return typeof answer === "number" && typeof value === "number" && answer > value;
|
|
296
|
+
case "greater-or-equal": return typeof answer === "number" && typeof value === "number" && answer >= value;
|
|
297
|
+
case "less": return typeof answer === "number" && typeof value === "number" && answer < value;
|
|
298
|
+
case "less-or-equal": return typeof answer === "number" && typeof value === "number" && answer <= value;
|
|
299
|
+
case "contains": return Array.isArray(answer) && typeof value === "string" && answer.includes(value);
|
|
300
|
+
case "not-contains": return Array.isArray(answer) && typeof value === "string" && !answer.includes(value);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const evaluateValue = (node) => {
|
|
304
|
+
return node.value;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/core/logic/validate.ts
|
|
309
|
+
const VALIDATOR_FACTORY_MAP = {
|
|
310
|
+
rating: ({ scale }, meta) => ratingAnswerSchemaFactory({ scale }, meta),
|
|
311
|
+
"open-ended": ({ max }, meta) => openEndedAnswerSchemaFactory({ maxLength: max }, meta),
|
|
312
|
+
"single-select": ({ editable }, meta) => singleSelectAnswerSchemaFactory({ maxLength: editable?.max }, meta),
|
|
313
|
+
"multi-select": ({ editable, max }, meta) => multiSelectAnswerSchemaFactory({
|
|
314
|
+
maxLength: editable?.max,
|
|
315
|
+
maxSelected: max
|
|
316
|
+
}, meta)
|
|
317
|
+
};
|
|
318
|
+
const getAnswerValidator = (properties, meta) => {
|
|
319
|
+
const type = properties.type;
|
|
320
|
+
switch (type) {
|
|
321
|
+
case "rating": {
|
|
322
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
323
|
+
return validator(properties, meta);
|
|
324
|
+
}
|
|
325
|
+
case "open-ended": {
|
|
326
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
327
|
+
return validator(properties, meta);
|
|
328
|
+
}
|
|
329
|
+
case "single-select": {
|
|
330
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
331
|
+
return validator(properties, meta);
|
|
332
|
+
}
|
|
333
|
+
case "multi-select": {
|
|
334
|
+
const validator = VALIDATOR_FACTORY_MAP[type];
|
|
335
|
+
return validator(properties, meta);
|
|
336
|
+
}
|
|
337
|
+
default: throw new Error("Unsupported question type");
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/core/ConfettiController.tsx
|
|
336
343
|
const getNextQuestionState = ({ originalQuestions, answers, errors, pointer = -1 }) => {
|
|
337
344
|
if (!originalQuestions) return {
|
|
338
345
|
questions: [],
|
|
@@ -465,13 +472,14 @@ const ConfettiController = ({ current: currentProp, shouldAutoSubmit, onFinished
|
|
|
465
472
|
|
|
466
473
|
//#endregion
|
|
467
474
|
//#region src/components/Confetti.tsx
|
|
468
|
-
const Confetti = ({ children, shouldAutoSubmit, ...props }) => {
|
|
475
|
+
const Confetti = ({ children, onFinished, shouldAutoSubmit, ...props }) => {
|
|
469
476
|
return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
470
477
|
fallback: /* @__PURE__ */ jsx(Fragment, {}),
|
|
471
478
|
children: /* @__PURE__ */ jsx(ConfettiProvider, {
|
|
472
479
|
...props,
|
|
473
480
|
children: /* @__PURE__ */ jsx(ConfettiController, {
|
|
474
481
|
shouldAutoSubmit,
|
|
482
|
+
onFinished,
|
|
475
483
|
children
|
|
476
484
|
})
|
|
477
485
|
})
|
|
@@ -481,29 +489,36 @@ const Confetti = ({ children, shouldAutoSubmit, ...props }) => {
|
|
|
481
489
|
//#endregion
|
|
482
490
|
//#region src/components/ThankYouMessage.tsx
|
|
483
491
|
const ThankYouMessage = ({ title, description, classNames }) => {
|
|
484
|
-
|
|
492
|
+
const { survey, dismiss } = useConfetti();
|
|
493
|
+
if (!survey) return null;
|
|
494
|
+
const descriptionText = description || survey.confirmation.description;
|
|
495
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
485
496
|
className: clsx("cf-question", classNames?.question),
|
|
486
497
|
children: /* @__PURE__ */ jsxs("div", {
|
|
487
498
|
className: clsx("cf-header", classNames?.header),
|
|
488
499
|
children: [/* @__PURE__ */ jsx(Label, {
|
|
489
500
|
className: clsx("cf-title", classNames?.title),
|
|
490
|
-
children: title
|
|
491
|
-
}), /* @__PURE__ */ jsx(Text, {
|
|
501
|
+
children: title || survey.confirmation.title || "Feedback submitted!"
|
|
502
|
+
}), descriptionText && /* @__PURE__ */ jsx(Text, {
|
|
492
503
|
slot: "description",
|
|
493
504
|
className: clsx("cf-description", classNames?.description),
|
|
494
|
-
children:
|
|
505
|
+
children: descriptionText
|
|
495
506
|
})]
|
|
496
507
|
})
|
|
497
|
-
})
|
|
508
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
509
|
+
className: "cf-submit-button",
|
|
510
|
+
onPress: dismiss,
|
|
511
|
+
children: survey.confirmation.buttonText || "Close"
|
|
512
|
+
})] });
|
|
498
513
|
};
|
|
499
514
|
|
|
500
515
|
//#endregion
|
|
501
516
|
//#region src/components/container/PopoverContainer.tsx
|
|
502
517
|
const PopoverContainer = ({ children, classNames, containerProps, onDismiss, closeOnFinishMs = 3e3 }) => {
|
|
503
|
-
const { surveyId, dismiss } = useConfetti();
|
|
518
|
+
const { surveyId, survey, dismiss } = useConfetti();
|
|
504
519
|
const [isFinished, setIsFinished] = useState(false);
|
|
505
520
|
useTimeout(() => {
|
|
506
|
-
dismiss();
|
|
521
|
+
if (survey) dismiss();
|
|
507
522
|
}, isFinished ? closeOnFinishMs : null);
|
|
508
523
|
return /* @__PURE__ */ jsxs("div", {
|
|
509
524
|
className: clsx("confetti-dialog", classNames?.dialog),
|
|
@@ -578,23 +593,29 @@ const FreeText = ({ id, title, description, properties, onChange, classNames, re
|
|
|
578
593
|
|
|
579
594
|
//#endregion
|
|
580
595
|
//#region src/components/questions/MultiChoice.tsx
|
|
581
|
-
const MultiChoiceCheckbox = ({ children, classNames, ...props }) => {
|
|
596
|
+
const MultiChoiceCheckbox = ({ children, classNames, description, ...props }) => {
|
|
582
597
|
return /* @__PURE__ */ jsx(Checkbox, {
|
|
583
598
|
...props,
|
|
584
|
-
children: ({ isIndeterminate }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */
|
|
585
|
-
className: clsx("cf-
|
|
586
|
-
children: /* @__PURE__ */ jsx("
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
599
|
+
children: ({ isIndeterminate }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
600
|
+
className: clsx("cf-choice-label", classNames?.label),
|
|
601
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
602
|
+
className: clsx("cf-checkbox-indicator", classNames?.indicator),
|
|
603
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
604
|
+
viewBox: "0 0 18 18",
|
|
605
|
+
"aria-hidden": "true",
|
|
606
|
+
className: clsx("cf-checkbox-indicator-svg", classNames?.svg),
|
|
607
|
+
children: isIndeterminate ? /* @__PURE__ */ jsx("rect", {
|
|
608
|
+
x: 1,
|
|
609
|
+
y: 7.5,
|
|
610
|
+
width: 16,
|
|
611
|
+
height: 3
|
|
612
|
+
}) : /* @__PURE__ */ jsx("polyline", { points: "2 9 7 14 16 4" })
|
|
613
|
+
}, isIndeterminate ? "indeterminate" : "check")
|
|
614
|
+
}), children]
|
|
615
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
616
|
+
className: clsx("cf-choice-description", classNames?.choiceDescription),
|
|
617
|
+
children: description
|
|
618
|
+
})] })
|
|
598
619
|
});
|
|
599
620
|
};
|
|
600
621
|
const MultiChoice = ({ title, description, properties, classNames, onChange, id, required, error }) => {
|
|
@@ -635,15 +656,16 @@ const MultiChoice = ({ title, description, properties, classNames, onChange, id,
|
|
|
635
656
|
/* @__PURE__ */ jsxs("div", {
|
|
636
657
|
className: clsx("cf-response-choices", classNames?.group),
|
|
637
658
|
children: [properties.options.map((option, index) => /* @__PURE__ */ jsx(MultiChoiceCheckbox, {
|
|
638
|
-
className: clsx("cf-checkbox", classNames?.checkbox),
|
|
659
|
+
className: clsx("cf-choice cf-checkbox", classNames?.checkbox),
|
|
639
660
|
value: option.value,
|
|
640
661
|
classNames: {
|
|
641
662
|
indicator: classNames?.indicator,
|
|
642
663
|
svg: classNames?.svg
|
|
643
664
|
},
|
|
644
|
-
|
|
665
|
+
description: option.description,
|
|
666
|
+
children: option.value
|
|
645
667
|
}, index)), hasEditable && /* @__PURE__ */ jsxs("div", {
|
|
646
|
-
className: clsx("cf-others-choice", classNames?.othersChoice),
|
|
668
|
+
className: clsx("cf-choice cf-others-choice", classNames?.othersChoice),
|
|
647
669
|
children: [/* @__PURE__ */ jsx(MultiChoiceCheckbox, {
|
|
648
670
|
inputRef: othersRef,
|
|
649
671
|
onChange: (isSelected) => {
|
|
@@ -659,6 +681,7 @@ const MultiChoice = ({ title, description, properties, classNames, onChange, id,
|
|
|
659
681
|
indicator: classNames?.indicator,
|
|
660
682
|
svg: classNames?.svg
|
|
661
683
|
},
|
|
684
|
+
description: properties.editable?.description,
|
|
662
685
|
children: properties.editable?.label
|
|
663
686
|
}), /* @__PURE__ */ jsx(Input, {
|
|
664
687
|
value: othersValue,
|
|
@@ -942,11 +965,17 @@ const Rating = ({ properties, title, description, classNames, onChange, id, erro
|
|
|
942
965
|
|
|
943
966
|
//#endregion
|
|
944
967
|
//#region src/components/questions/SingleChoice.tsx
|
|
945
|
-
const SingleChoiceRadio = ({ classNames, className, ...props }) => {
|
|
968
|
+
const SingleChoiceRadio = ({ classNames, className, description, ...props }) => {
|
|
946
969
|
return /* @__PURE__ */ jsx(Radio, {
|
|
947
970
|
...props,
|
|
948
971
|
className: clsx(className, classNames?.radio),
|
|
949
|
-
children: composeRenderProps(props.children, (children) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */
|
|
972
|
+
children: composeRenderProps(props.children, (children) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
973
|
+
className: clsx("cf-choice-label", classNames?.label),
|
|
974
|
+
children: [/* @__PURE__ */ jsx("div", { className: clsx("cf-radio-indicator", classNames?.indicator) }), children]
|
|
975
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
976
|
+
className: clsx("cf-choice-description", classNames?.choiceDescription),
|
|
977
|
+
children: description
|
|
978
|
+
})] }))
|
|
950
979
|
});
|
|
951
980
|
};
|
|
952
981
|
const SingleChoice = ({ title, description, properties, classNames, onChange, id, required, error }) => {
|
|
@@ -992,12 +1021,13 @@ const SingleChoice = ({ title, description, properties, classNames, onChange, id
|
|
|
992
1021
|
children: [properties.options.map((option, index) => /* @__PURE__ */ jsx(SingleChoiceRadio, {
|
|
993
1022
|
classNames: {
|
|
994
1023
|
indicator: classNames?.indicator,
|
|
995
|
-
radio: clsx("cf-radio", classNames?.radio)
|
|
1024
|
+
radio: clsx("cf-choice cf-radio", classNames?.radio)
|
|
996
1025
|
},
|
|
997
1026
|
value: option.value,
|
|
998
|
-
|
|
1027
|
+
description: option.description,
|
|
1028
|
+
children: option.value
|
|
999
1029
|
}, index)), hasEditable && /* @__PURE__ */ jsxs("div", {
|
|
1000
|
-
className: clsx("cf-others-choice", classNames?.othersChoice),
|
|
1030
|
+
className: clsx("cf-choice cf-others-choice", classNames?.othersChoice),
|
|
1001
1031
|
children: [/* @__PURE__ */ jsx(SingleChoiceRadio, {
|
|
1002
1032
|
inputRef: othersRef,
|
|
1003
1033
|
value: RADIO_OTHERS_VALUE,
|
|
@@ -1005,6 +1035,7 @@ const SingleChoice = ({ title, description, properties, classNames, onChange, id
|
|
|
1005
1035
|
indicator: classNames?.indicator,
|
|
1006
1036
|
radio: clsx("cf-radio", classNames?.radio)
|
|
1007
1037
|
},
|
|
1038
|
+
description: properties.editable?.description,
|
|
1008
1039
|
children: properties.editable?.label
|
|
1009
1040
|
}), /* @__PURE__ */ jsx(Input, {
|
|
1010
1041
|
tabIndex: state.selectedValue === RADIO_OTHERS_VALUE ? 0 : -1,
|
|
@@ -1126,6 +1157,40 @@ const EmbeddedConfetti = (props) => {
|
|
|
1126
1157
|
});
|
|
1127
1158
|
};
|
|
1128
1159
|
|
|
1160
|
+
//#endregion
|
|
1161
|
+
//#region src/templates/PopoverConfetti.tsx
|
|
1162
|
+
const PopoverConfetti = ({ closeOnFinishMs, ...props }) => {
|
|
1163
|
+
return /* @__PURE__ */ jsx(ConfettiProvider, {
|
|
1164
|
+
...props,
|
|
1165
|
+
children: /* @__PURE__ */ jsx(PopoverContainer, {
|
|
1166
|
+
closeOnFinishMs,
|
|
1167
|
+
children: ({ setIsFinished }) => /* @__PURE__ */ jsx(ConfettiController, {
|
|
1168
|
+
shouldAutoSubmit: true,
|
|
1169
|
+
...props,
|
|
1170
|
+
onFinished: () => {
|
|
1171
|
+
setIsFinished();
|
|
1172
|
+
props.onFinished?.();
|
|
1173
|
+
},
|
|
1174
|
+
children: ({ current, questions, update, isSubmitted }) => {
|
|
1175
|
+
if (isSubmitted) return /* @__PURE__ */ jsx(ThankYouMessage, {});
|
|
1176
|
+
const currentQuestion = questions[current];
|
|
1177
|
+
if (!currentQuestion) return /* @__PURE__ */ jsx(Fragment, {});
|
|
1178
|
+
return /* @__PURE__ */ jsx(Flow, {
|
|
1179
|
+
question: currentQuestion,
|
|
1180
|
+
onSubmit: (answer) => {
|
|
1181
|
+
return update({
|
|
1182
|
+
question: current,
|
|
1183
|
+
answer
|
|
1184
|
+
});
|
|
1185
|
+
},
|
|
1186
|
+
children: (flowProps) => /* @__PURE__ */ jsx(SurveyQuestionFactory, { ...flowProps })
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
})
|
|
1190
|
+
})
|
|
1191
|
+
});
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1129
1194
|
//#endregion
|
|
1130
1195
|
//#region src/templates/StepperConfetti.tsx
|
|
1131
1196
|
const StepperConfetti = (props) => {
|
|
@@ -1151,5 +1216,59 @@ const StepperConfetti = (props) => {
|
|
|
1151
1216
|
};
|
|
1152
1217
|
|
|
1153
1218
|
//#endregion
|
|
1154
|
-
|
|
1219
|
+
//#region src/hooks/useVisibleAfterDelay.ts
|
|
1220
|
+
const useVisibleAfterDelay = ({ delay }) => {
|
|
1221
|
+
const [isVisible$1, setIsVisible] = useState(false);
|
|
1222
|
+
const initialDelay = useMemo(() => delay, []);
|
|
1223
|
+
useEffect(() => {
|
|
1224
|
+
const timeoutId = setTimeout(() => {
|
|
1225
|
+
setIsVisible(true);
|
|
1226
|
+
}, initialDelay);
|
|
1227
|
+
return () => clearTimeout(timeoutId);
|
|
1228
|
+
}, []);
|
|
1229
|
+
return { isVisible: isVisible$1 };
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
//#endregion
|
|
1233
|
+
//#region src/hooks/useVisibleAfterPageVisits.ts
|
|
1234
|
+
const useVisibleAfterPageVisits = ({ visits, respondent }) => {
|
|
1235
|
+
const [visitCount, setVisitCount] = useLocalStorage(`cfti:${useMemo(() => window.location.pathname, [])}:visits:${respondent ?? ""}`, 0);
|
|
1236
|
+
useEffect(() => {
|
|
1237
|
+
setVisitCount((prev) => prev + 1);
|
|
1238
|
+
}, [setVisitCount]);
|
|
1239
|
+
return {
|
|
1240
|
+
isVisible: visitCount >= visits,
|
|
1241
|
+
reset: useCallback(() => setVisitCount(0), [setVisitCount])
|
|
1242
|
+
};
|
|
1243
|
+
};
|
|
1244
|
+
const useVisibleAfterSessionPageVisits = ({ visits, respondent }) => {
|
|
1245
|
+
const [visitCount, setVisitCount] = useSessionStorage(`cfti:${useMemo(() => window.location.pathname, [])}:visits:${respondent ?? ""}`, 0);
|
|
1246
|
+
useEffect(() => {
|
|
1247
|
+
setVisitCount((prev) => prev + 1);
|
|
1248
|
+
}, [setVisitCount]);
|
|
1249
|
+
return {
|
|
1250
|
+
isVisible: visitCount >= visits,
|
|
1251
|
+
reset: useCallback(() => setVisitCount(0), [setVisitCount])
|
|
1252
|
+
};
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
//#endregion
|
|
1256
|
+
//#region src/hooks/useVisibleAfterScroll.ts
|
|
1257
|
+
const useVisibleAfterScroll = ({ threshold }) => {
|
|
1258
|
+
const [isVisible$1, setIsVisible] = useState(() => {
|
|
1259
|
+
if (typeof window === "undefined") return false;
|
|
1260
|
+
return window.scrollY >= threshold;
|
|
1261
|
+
});
|
|
1262
|
+
useEffect(() => {
|
|
1263
|
+
const handleScroll = () => {
|
|
1264
|
+
if (window.scrollY >= threshold) setIsVisible(true);
|
|
1265
|
+
};
|
|
1266
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
1267
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
1268
|
+
}, [threshold]);
|
|
1269
|
+
return { isVisible: isVisible$1 };
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
//#endregion
|
|
1273
|
+
export { Confetti, ConfettiContext, ConfettiController, ConfettiProvider, Embed, EmbeddedConfetti, Flow, FreeText, MultiChoice, PopoverConfetti, PopoverContainer, Rating, SingleChoice, StepperConfetti, SurveyQuestionFactory, ThankYouMessage, useConfetti, useVisibleAfterDelay, useVisibleAfterPageVisits, useVisibleAfterScroll, useVisibleAfterSessionPageVisits };
|
|
1155
1274
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengovsg/confetti",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Widgets for Confetti",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"prettier": "^3.7.4",
|
|
39
39
|
"tsdown": "0.19.0",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
|
-
"@confetti/validators": "0.0.0",
|
|
42
41
|
"@confetti/tsconfig": "0.0.0",
|
|
42
|
+
"@confetti/validators": "0.0.0",
|
|
43
43
|
"@confetti/eslint-config": "0.0.0",
|
|
44
44
|
"@confetti/prettier-config": "0.0.0"
|
|
45
45
|
},
|