@explorer02/cfm-survey-sdk 0.2.1 → 0.2.3
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/cli/index.js +18 -18
- package/dist/cli/index.mjs +14 -14
- package/dist/index.d.mts +465 -580
- package/dist/index.d.ts +465 -580
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -4
- package/postinstall.js +96 -69
- package/templates/AGENT.md +18 -8
- package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
- package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
- package/templates/docs/00-integration/aws-deploy.md +281 -0
- package/templates/docs/00-integration/client-integration-guide.md +351 -0
- package/templates/docs/00-integration/component-checklist.md +55 -22
- package/templates/docs/00-integration/constraints.md +49 -4
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +64 -0
- package/templates/docs/00-integration/end-page-logic.md +64 -0
- package/templates/docs/00-integration/file-upload-aws.md +116 -0
- package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
- package/templates/docs/00-integration/partial-save-and-recovery.md +98 -0
- package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
- package/templates/docs/00-integration/progress.md +61 -0
- package/templates/docs/00-integration/question-display-variants.md +65 -0
- package/templates/docs/00-integration/question-numbering.md +51 -0
- package/templates/docs/00-integration/question-type-sdk-matrix.md +244 -0
- package/templates/docs/00-integration/setup.md +84 -0
- package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
- package/templates/docs/00-integration/useSurveySDK.md +151 -23
- package/templates/docs/01-components/01-survey-page.md +76 -54
- package/templates/docs/01-components/02-question.md +61 -58
- package/templates/docs/01-components/03-rating-scale.md +11 -1
- package/templates/docs/01-components/04-star-rating-scale.md +45 -0
- package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
- package/templates/docs/01-components/06-likert-matrix-scale.md +14 -4
- package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
- package/templates/docs/01-components/08-file-upload-scale.md +23 -51
- package/templates/docs/01-components/09-custom-slider-track.md +2 -2
- package/templates/docs/01-components/11-progress-bar.md +10 -4
- package/templates/docs/01-components/13-matrix-dropdown.md +29 -32
- package/templates/docs/01-components/14-intro-page.md +34 -0
- package/templates/docs/01-components/15-end-page.md +31 -0
- package/templates/docs/01-components/16-paused-page.md +27 -0
- package/templates/docs/01-components/17-heatmap-scale.md +75 -0
- package/templates/docs/01-components/18-rank-order-scale.md +93 -0
- package/templates/docs/01-components/README.md +42 -36
- package/templates/docs/02-reference/config-field-index.md +141 -139
- package/templates/docs/02-reference/exports.md +149 -0
- package/templates/docs/02-reference/question-types/01-rating.md +31 -32
- package/templates/docs/02-reference/question-types/04-csat.md +51 -42
- package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
- package/templates/docs/02-reference/question-types/06-slider.md +2 -2
- package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
- package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
- package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
- package/templates/docs/02-reference/question-types/11-file-upload.md +43 -17
- package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
- package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
- package/templates/docs/02-reference/question-types/README.md +96 -47
- package/templates/docs/02-reference/routing-table.md +33 -18
- package/templates/docs/02-reference/value-derivation.md +63 -0
- package/templates/docs/03-ui-specs/00-question-shell.md +19 -88
- package/templates/docs/03-ui-specs/01-rating.md +18 -52
- package/templates/docs/03-ui-specs/02-radio.md +80 -37
- package/templates/docs/03-ui-specs/03-text.md +2 -2
- package/templates/docs/03-ui-specs/04-csat.md +68 -33
- package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +44 -25
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +19 -43
- package/templates/docs/03-ui-specs/09-slider-matrix.md +18 -1
- package/templates/docs/03-ui-specs/10-file-upload.md +42 -21
- package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
- package/templates/docs/03-ui-specs/12-survey-chrome.md +58 -14
- package/templates/docs/03-ui-specs/13-heatmap.md +80 -0
- package/templates/docs/03-ui-specs/14-rank-order.md +128 -0
- package/templates/docs/03-ui-specs/README.md +16 -14
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +5 -0
- package/templates/docs/MANIFEST.json +476 -69
- package/templates/docs/index.md +82 -30
- package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
- package/templates/docs/templates/MatrixDropdown.tsx +216 -0
- package/templates/docs/templates/Question.tsx +221 -154
- package/templates/docs/templates/RankOrderScale.tsx +353 -0
- package/templates/docs/templates/deploy-to-aws.sh +114 -0
- package/templates/docs/templates/heatmapCoords.ts +58 -0
- package/templates/docs/templates/implementation_plan.md +126 -67
- package/templates/docs/templates/survey-inventory.schema.json +77 -0
- package/templates/docs/templates/surveyUiIcons.tsx +52 -0
- package/templates/docs/templates/verify-agent-build.sh +85 -0
- package/templates/docs/01-components/04-csat-scale.md +0 -40
- package/templates/docs/02-question-types/01-rating.md +0 -44
|
@@ -2,52 +2,118 @@
|
|
|
2
2
|
* CANONICAL QUESTION DISPATCHER TEMPLATE
|
|
3
3
|
*
|
|
4
4
|
* Agents: copy this file to your project's components folder (e.g. src/components/Question.tsx).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* docs/03-ui-specs/00-question-shell.md
|
|
8
|
-
*
|
|
9
|
-
* RULES:
|
|
10
|
-
* - Do NOT add a default/fallback branch that renders "not yet implemented" or similar stub text.
|
|
11
|
-
* - matrix MUST route by question.subType — type alone is NOT enough.
|
|
12
|
-
* - If a question type appears in the fetched survey, its branch MUST render a real interactive component.
|
|
5
|
+
* Covers all 11 QUESTION_TYPE literals — see question-type-sdk-matrix.md for per-type wiring.
|
|
6
|
+
* Route on `question.type` / `QUESTION_TYPE` only — no subType.
|
|
13
7
|
*
|
|
14
8
|
* Replace @explorer02/cfm-survey-sdk with your package.json import name if different.
|
|
15
9
|
*/
|
|
16
10
|
'use client';
|
|
17
11
|
|
|
18
|
-
import React from 'react';
|
|
19
|
-
import type { SurveyQuestion, AnswerValue } from '@explorer02/cfm-survey-sdk';
|
|
20
|
-
import {
|
|
12
|
+
import React, { useMemo } from 'react';
|
|
13
|
+
import type { SurveyQuestion, AnswerValue, SurveyAnswers, CustomFieldValueMap, HeatmapAnswer, HeatmapQuestion } from '@explorer02/cfm-survey-sdk';
|
|
14
|
+
import { QUESTION_TYPE, resolveSurveyQuestionPlaceholders, getVisibleMatrixItemsForAnswers, getVisibleMcqOptionsForAnswers, getVisibleRankOrderOptionsForAnswers } from '@explorer02/cfm-survey-sdk';
|
|
21
15
|
import { CsatMatrixScale } from './CsatMatrixScale';
|
|
22
16
|
import { SliderMatrixScale } from './SliderMatrixScale';
|
|
23
17
|
import { LikertMatrixScale } from './LikertMatrixScale';
|
|
24
18
|
import { FileUploadScale } from './FileUploadScale';
|
|
19
|
+
import { HeatmapScale } from './HeatmapScale';
|
|
20
|
+
import { RankOrderScale } from './RankOrderScale';
|
|
25
21
|
import RatingScale from './RatingScale';
|
|
26
|
-
// rating_scale / slider gaps — create these if survey contains those types:
|
|
27
|
-
// import { CustomSliderTrack } from './CustomSliderTrack';
|
|
28
22
|
|
|
29
23
|
type QuestionProps = {
|
|
30
24
|
question: SurveyQuestion;
|
|
31
25
|
selectedValue?: AnswerValue;
|
|
32
26
|
validationError?: string;
|
|
33
|
-
|
|
27
|
+
allAnswers: SurveyAnswers;
|
|
28
|
+
allQuestions: SurveyQuestion[];
|
|
29
|
+
customFieldValues?: CustomFieldValueMap;
|
|
30
|
+
onSelect: (answerValue: AnswerValue) => void;
|
|
34
31
|
};
|
|
35
32
|
|
|
36
|
-
export default function Question({
|
|
33
|
+
export default function Question({
|
|
34
|
+
question,
|
|
35
|
+
selectedValue,
|
|
36
|
+
validationError,
|
|
37
|
+
allAnswers,
|
|
38
|
+
allQuestions,
|
|
39
|
+
customFieldValues = {},
|
|
40
|
+
onSelect,
|
|
41
|
+
}: QuestionProps) {
|
|
42
|
+
const questionsById = useMemo(
|
|
43
|
+
() => new Map(allQuestions.map(q => [q.id, q])),
|
|
44
|
+
[allQuestions]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const displayQuestionText = useMemo(
|
|
48
|
+
() => resolveSurveyQuestionPlaceholders(question.questionText, allAnswers, questionsById),
|
|
49
|
+
[question.questionText, allAnswers, questionsById]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const displayQuestionDescription = useMemo(
|
|
53
|
+
() =>
|
|
54
|
+
question.questionDescription
|
|
55
|
+
? resolveSurveyQuestionPlaceholders(question.questionDescription, allAnswers, questionsById)
|
|
56
|
+
: undefined,
|
|
57
|
+
[question.questionDescription, allAnswers, questionsById]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const matrixQuestion = useMemo(() => {
|
|
61
|
+
const isMatrix =
|
|
62
|
+
question.type === QUESTION_TYPE.CFM_MATRIX ||
|
|
63
|
+
question.type === QUESTION_TYPE.CSAT_MATRIX ||
|
|
64
|
+
question.type === QUESTION_TYPE.RATING_MATRIX ||
|
|
65
|
+
question.type === QUESTION_TYPE.SLIDER_MATRIX;
|
|
66
|
+
|
|
67
|
+
if (!isMatrix) return question;
|
|
68
|
+
|
|
69
|
+
const { statementRows, scaleColumns } = getVisibleMatrixItemsForAnswers(
|
|
70
|
+
question,
|
|
71
|
+
allAnswers,
|
|
72
|
+
allQuestions,
|
|
73
|
+
customFieldValues
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...question,
|
|
78
|
+
statementRows,
|
|
79
|
+
...(question.type === QUESTION_TYPE.CSAT_MATRIX || question.type === QUESTION_TYPE.CFM_MATRIX
|
|
80
|
+
? { scaleColumns }
|
|
81
|
+
: {}),
|
|
82
|
+
};
|
|
83
|
+
}, [question, allAnswers, allQuestions, customFieldValues]);
|
|
84
|
+
|
|
85
|
+
const visibleMcqOptions = useMemo(() => {
|
|
86
|
+
if (question.type !== QUESTION_TYPE.MCQ) return [];
|
|
87
|
+
return getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues);
|
|
88
|
+
}, [question, allAnswers, allQuestions, customFieldValues]);
|
|
89
|
+
|
|
90
|
+
const rankOrderQuestion = useMemo(() => {
|
|
91
|
+
if (question.type !== QUESTION_TYPE.RANK_ORDER) return null;
|
|
92
|
+
return {
|
|
93
|
+
...question,
|
|
94
|
+
options: getVisibleRankOrderOptionsForAnswers(
|
|
95
|
+
question,
|
|
96
|
+
allAnswers,
|
|
97
|
+
allQuestions,
|
|
98
|
+
customFieldValues
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
}, [question, allAnswers, allQuestions, customFieldValues]);
|
|
102
|
+
|
|
37
103
|
return (
|
|
38
104
|
<section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
39
105
|
<h2 className="text-lg font-medium leading-relaxed text-gray-900">
|
|
40
|
-
<span dangerouslySetInnerHTML={{ __html:
|
|
106
|
+
<span dangerouslySetInnerHTML={{ __html: displayQuestionText }} />
|
|
41
107
|
{question.required && <span className="text-red-500 ml-1">*</span>}
|
|
42
108
|
</h2>
|
|
43
|
-
{
|
|
109
|
+
{displayQuestionDescription && (
|
|
44
110
|
<div
|
|
45
111
|
className="text-sm text-gray-600 mt-2 leading-relaxed"
|
|
46
|
-
dangerouslySetInnerHTML={{ __html:
|
|
112
|
+
dangerouslySetInnerHTML={{ __html: displayQuestionDescription }}
|
|
47
113
|
/>
|
|
48
114
|
)}
|
|
49
115
|
|
|
50
|
-
{question.type ===
|
|
116
|
+
{question.type === QUESTION_TYPE.NPS_SCALE && (
|
|
51
117
|
<div className="space-y-3">
|
|
52
118
|
{(question.minLabel || question.midLabel || question.maxLabel) && (
|
|
53
119
|
<div className="relative w-full h-6 text-xs font-semibold text-gray-500">
|
|
@@ -60,9 +126,8 @@ export default function Question({ question, selectedValue, validationError, onS
|
|
|
60
126
|
className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
|
|
61
127
|
style={{
|
|
62
128
|
left: `${question.midLabelIndex !== undefined && question.options.length > 1
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}%`,
|
|
129
|
+
? (question.midLabelIndex / (question.options.length - 1)) * 100
|
|
130
|
+
: 50}%`,
|
|
66
131
|
}}
|
|
67
132
|
dangerouslySetInnerHTML={{ __html: question.midLabel }}
|
|
68
133
|
/>
|
|
@@ -78,164 +143,166 @@ export default function Question({ question, selectedValue, validationError, onS
|
|
|
78
143
|
options={question.options}
|
|
79
144
|
selectedValue={selectedValue}
|
|
80
145
|
onSelect={onSelect}
|
|
146
|
+
variant="nps"
|
|
147
|
+
buttonStyle={question.buttonStyle}
|
|
81
148
|
/>
|
|
82
149
|
</div>
|
|
83
150
|
)}
|
|
84
151
|
|
|
85
|
-
{question.type ===
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
152
|
+
{question.type === QUESTION_TYPE.MCQ && (() => {
|
|
153
|
+
const effectiveValue =
|
|
154
|
+
selectedValue ??
|
|
155
|
+
(question.selectionMode === 'single'
|
|
156
|
+
? question.defaultOptionIds?.[0]
|
|
157
|
+
: question.defaultOptionIds);
|
|
158
|
+
|
|
159
|
+
if (question.selectionMode === 'multiple') {
|
|
160
|
+
const selectedIds: string[] = Array.isArray(effectiveValue)
|
|
161
|
+
? effectiveValue.filter((id): id is string => typeof id === 'string')
|
|
162
|
+
: typeof effectiveValue === 'string'
|
|
163
|
+
? [effectiveValue]
|
|
164
|
+
: [];
|
|
165
|
+
|
|
166
|
+
const handleToggle = (optionId: string) => {
|
|
167
|
+
const next = selectedIds.includes(optionId)
|
|
168
|
+
? selectedIds.filter(id => id !== optionId)
|
|
169
|
+
: question.maxSelections && selectedIds.length >= question.maxSelections
|
|
170
|
+
? selectedIds
|
|
171
|
+
: [...selectedIds, optionId];
|
|
172
|
+
onSelect(next);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div className="space-y-3">
|
|
177
|
+
{visibleMcqOptions.map(option => {
|
|
178
|
+
const isSelected = selectedIds.includes(option.id);
|
|
179
|
+
return (
|
|
180
|
+
<label
|
|
181
|
+
key={option.id}
|
|
182
|
+
className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
|
|
183
|
+
>
|
|
184
|
+
<input
|
|
185
|
+
type="checkbox"
|
|
186
|
+
name={question.id}
|
|
187
|
+
checked={isSelected}
|
|
188
|
+
onChange={() => handleToggle(option.id)}
|
|
189
|
+
className="sr-only"
|
|
190
|
+
/>
|
|
191
|
+
<span
|
|
192
|
+
className="text-[15px] font-medium text-gray-900 leading-tight"
|
|
193
|
+
dangerouslySetInnerHTML={{ __html: option.optionLabel }}
|
|
194
|
+
/>
|
|
195
|
+
</label>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const singleSelected = typeof effectiveValue === 'string' ? effectiveValue : undefined;
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="space-y-3">
|
|
206
|
+
{visibleMcqOptions.map(option => {
|
|
207
|
+
const isSelected = singleSelected === option.id;
|
|
208
|
+
return (
|
|
209
|
+
<label
|
|
210
|
+
key={option.id}
|
|
211
|
+
className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
|
|
106
212
|
>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
213
|
+
<input
|
|
214
|
+
type="radio"
|
|
215
|
+
name={question.id}
|
|
216
|
+
checked={isSelected}
|
|
217
|
+
onChange={() => onSelect(option.id)}
|
|
218
|
+
className="sr-only"
|
|
219
|
+
/>
|
|
220
|
+
<span
|
|
221
|
+
className="text-[15px] font-medium text-gray-900 leading-tight"
|
|
222
|
+
dangerouslySetInnerHTML={{ __html: option.optionLabel }}
|
|
223
|
+
/>
|
|
224
|
+
</label>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
})()}
|
|
230
|
+
|
|
231
|
+
{question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
|
|
232
|
+
<RankOrderScale
|
|
233
|
+
question={rankOrderQuestion}
|
|
234
|
+
selectedValue={
|
|
235
|
+
typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
|
|
236
|
+
? selectedValue
|
|
237
|
+
: undefined
|
|
238
|
+
}
|
|
239
|
+
onSelect={onSelect}
|
|
240
|
+
/>
|
|
117
241
|
)}
|
|
118
242
|
|
|
119
|
-
{question.type ===
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/>
|
|
128
|
-
<div className="text-right text-xs text-gray-500 pr-1">
|
|
129
|
-
{Math.max(
|
|
130
|
-
0,
|
|
131
|
-
(question.maxCharacterCount ?? 1000) - (typeof selectedValue === 'string' ? selectedValue.length : 0)
|
|
132
|
-
)}{' '}
|
|
133
|
-
Zeichen verbleiben.
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
243
|
+
{question.type === QUESTION_TYPE.TEXTFIELD && (
|
|
244
|
+
<textarea
|
|
245
|
+
rows={question.inputVariant === 'long' ? 4 : 1}
|
|
246
|
+
value={(selectedValue as string) ?? ''}
|
|
247
|
+
onChange={e => onSelect(e.target.value)}
|
|
248
|
+
placeholder={question.placeholder || 'Type your response here...'}
|
|
249
|
+
className="w-full rounded border border-gray-300 p-4 text-sm outline-none focus:border-[#e20074]"
|
|
250
|
+
/>
|
|
136
251
|
)}
|
|
137
252
|
|
|
138
|
-
{question.type ===
|
|
139
|
-
<
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
<CsatScale
|
|
147
|
-
question={question}
|
|
148
|
-
selectedValue={selectedValue as string | number | null}
|
|
149
|
-
onSelect={onSelect}
|
|
150
|
-
/>
|
|
151
|
-
</div>
|
|
253
|
+
{question.type === QUESTION_TYPE.CFM_MATRIX && (
|
|
254
|
+
<LikertMatrixScale
|
|
255
|
+
question={matrixQuestion as typeof question & { type: typeof QUESTION_TYPE.CFM_MATRIX }}
|
|
256
|
+
selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
|
|
257
|
+
onSelect={onSelect}
|
|
258
|
+
/>
|
|
152
259
|
)}
|
|
153
260
|
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
/>
|
|
162
|
-
</div>
|
|
261
|
+
{(question.type === QUESTION_TYPE.CSAT_MATRIX ||
|
|
262
|
+
question.type === QUESTION_TYPE.RATING_MATRIX) && (
|
|
263
|
+
<CsatMatrixScale
|
|
264
|
+
question={matrixQuestion as Extract<SurveyQuestion, { type: typeof QUESTION_TYPE.CSAT_MATRIX | typeof QUESTION_TYPE.RATING_MATRIX }>}
|
|
265
|
+
selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
|
|
266
|
+
onSelect={onSelect}
|
|
267
|
+
/>
|
|
163
268
|
)}
|
|
164
269
|
|
|
165
|
-
{question.type ===
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
/>
|
|
172
|
-
</div>
|
|
270
|
+
{question.type === QUESTION_TYPE.SLIDER_MATRIX && (
|
|
271
|
+
<SliderMatrixScale
|
|
272
|
+
question={matrixQuestion as typeof question & { type: typeof QUESTION_TYPE.SLIDER_MATRIX }}
|
|
273
|
+
selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
|
|
274
|
+
onSelect={onSelect}
|
|
275
|
+
/>
|
|
173
276
|
)}
|
|
174
277
|
|
|
175
|
-
{question.type ===
|
|
176
|
-
<
|
|
177
|
-
<SliderMatrixScale
|
|
178
|
-
question={question}
|
|
179
|
-
selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
|
|
180
|
-
onSelect={onSelect}
|
|
181
|
-
/>
|
|
182
|
-
</div>
|
|
278
|
+
{question.type === QUESTION_TYPE.FILE_UPLOAD && (
|
|
279
|
+
<FileUploadScale question={question} selectedValue={selectedValue} onSelect={onSelect} />
|
|
183
280
|
)}
|
|
184
281
|
|
|
185
|
-
{question.type ===
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
/>
|
|
192
|
-
</div>
|
|
282
|
+
{question.type === QUESTION_TYPE.HEATMAP && (
|
|
283
|
+
<HeatmapScale
|
|
284
|
+
question={question as HeatmapQuestion}
|
|
285
|
+
selectedValue={Array.isArray(selectedValue) ? (selectedValue as HeatmapAnswer) : undefined}
|
|
286
|
+
onSelect={onSelect}
|
|
287
|
+
/>
|
|
193
288
|
)}
|
|
194
289
|
|
|
195
|
-
{question.type ===
|
|
196
|
-
<div className={`mt-4 flex w-full ${
|
|
197
|
-
question.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
? 'justify-end'
|
|
201
|
-
: 'justify-start'
|
|
202
|
-
}`}>
|
|
203
|
-
<div
|
|
204
|
-
style={{ width: question.mediaSize ? `${question.mediaSize}%` : '100%' }}
|
|
205
|
-
className="flex flex-col gap-2"
|
|
206
|
-
>
|
|
207
|
-
{question.mediaMimeType?.startsWith('video/') || question.mediaMimeType === 'VIDEO' ? (
|
|
208
|
-
<video controls src={question.mediaUrl} className="max-w-full rounded-md shadow-sm" />
|
|
290
|
+
{question.type === QUESTION_TYPE.TEXT_AND_MEDIA && question.media?.url && (
|
|
291
|
+
<div className={`mt-4 flex w-full ${question.media.alignment?.includes('CENTER') ? 'justify-center' : question.media.alignment?.includes('RIGHT') ? 'justify-end' : 'justify-start'}`}>
|
|
292
|
+
<div style={{ width: question.media.size ? `${question.media.size}%` : '100%' }}>
|
|
293
|
+
{question.media.mimeType?.startsWith('video/') || question.media.mimeType === 'VIDEO' ? (
|
|
294
|
+
<video controls src={question.media.url} className="max-w-full rounded-md" />
|
|
209
295
|
) : (
|
|
210
|
-
<img src={question.
|
|
211
|
-
)}
|
|
212
|
-
{question.mediaTitle && (
|
|
213
|
-
<p className="text-sm text-gray-500 text-center">{question.mediaTitle}</p>
|
|
296
|
+
<img src={question.media.url} alt={question.media.title || ''} className="max-w-full rounded-md" />
|
|
214
297
|
)}
|
|
298
|
+
{question.media.title && <p className="text-sm text-gray-500 text-center mt-2">{question.media.title}</p>}
|
|
215
299
|
</div>
|
|
216
300
|
</div>
|
|
217
301
|
)}
|
|
218
302
|
|
|
219
|
-
{/* rating_scale / slider: implement per 03-ui-specs/05-rating-scale.md and 06-slider.md if present in survey */}
|
|
220
|
-
{question.type === 'rating_scale' && (
|
|
221
|
-
<div className="space-y-3">
|
|
222
|
-
<CsatScale
|
|
223
|
-
question={{ ...question, type: 'csat', buttonType: question.scaleStyle === 'star' ? 'star' : 'emoji' }}
|
|
224
|
-
selectedValue={selectedValue as number | null}
|
|
225
|
-
onSelect={onSelect}
|
|
226
|
-
/>
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
229
|
-
|
|
230
|
-
{question.type === 'slider' && (
|
|
231
|
-
<div className="space-y-3">
|
|
232
|
-
{/* Wire CustomSliderTrack — min/max/step from question; see 03-ui-specs/06-slider.md */}
|
|
233
|
-
</div>
|
|
234
|
-
)}
|
|
235
|
-
|
|
236
303
|
{validationError && (
|
|
237
|
-
<div className="mt-4 rounded
|
|
238
|
-
<p className="text-
|
|
304
|
+
<div className="mt-4 rounded border border-gray-800 bg-gray-200 py-3 px-4">
|
|
305
|
+
<p className="text-sm font-bold text-gray-900">{validationError}</p>
|
|
239
306
|
</div>
|
|
240
307
|
)}
|
|
241
308
|
</section>
|