@explorer02/cfm-survey-sdk 0.1.3 → 0.1.5
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/package.json +1 -1
- package/postinstall.js +30 -18
- package/templates/AGENT.md +10 -1906
- package/templates/docs/01-sdk-core/01-fetch-survey.md +68 -0
- package/templates/docs/01-sdk-core/02-survey-mapper.md +85 -0
- package/templates/docs/01-sdk-core/03-question-mappers.md +114 -0
- package/templates/docs/01-sdk-core/04-pagination.md +72 -0
- package/templates/docs/01-sdk-core/05-validation.md +66 -0
- package/templates/docs/01-sdk-core/06-submit-survey.md +90 -0
- package/templates/docs/01-sdk-core/07-language-handling.md +111 -0
- package/templates/docs/01-sdk-core/08-icons-and-emojis.md +88 -0
- package/templates/docs/01-sdk-core/README.md +53 -0
- package/templates/docs/02-question-types/01-rating.md +52 -0
- package/templates/docs/02-question-types/02-radio.md +26 -0
- package/templates/docs/02-question-types/03-text.md +26 -0
- package/templates/docs/02-question-types/04-csat.md +54 -0
- package/templates/docs/02-question-types/05-rating-scale.md +26 -0
- package/templates/docs/02-question-types/06-slider.md +30 -0
- package/templates/docs/02-question-types/07-matrix-cfm.md +43 -0
- package/templates/docs/02-question-types/08-matrix-csat.md +29 -0
- package/templates/docs/02-question-types/09-matrix-rating.md +28 -0
- package/templates/docs/02-question-types/10-slider-matrix.md +40 -0
- package/templates/docs/02-question-types/11-file-upload.md +34 -0
- package/templates/docs/02-question-types/12-text-and-media.md +35 -0
- package/templates/docs/02-question-types/README.md +74 -0
- package/templates/docs/03-client-components/01-survey-page.md +113 -0
- package/templates/docs/03-client-components/02-question.md +57 -0
- package/templates/docs/03-client-components/03-rating-scale.md +38 -0
- package/templates/docs/03-client-components/04-csat-scale.md +40 -0
- package/templates/docs/03-client-components/05-csat-matrix-scale.md +43 -0
- package/templates/docs/03-client-components/06-likert-matrix-scale.md +38 -0
- package/templates/docs/03-client-components/07-slider-matrix-scale.md +44 -0
- package/templates/docs/03-client-components/08-file-upload-scale.md +33 -0
- package/templates/docs/03-client-components/09-custom-slider-track.md +27 -0
- package/templates/docs/03-client-components/10-header-footer.md +46 -0
- package/templates/docs/03-client-components/11-progress-bar.md +31 -0
- package/templates/docs/03-client-components/12-language-selector.md +41 -0
- package/templates/docs/03-client-components/13-matrix-dropdown.md +42 -0
- package/templates/docs/03-client-components/README.md +63 -0
- package/templates/docs/04-critical-rules/01-import-rules.md +51 -0
- package/templates/docs/04-critical-rules/02-action-dispatching.md +56 -0
- package/templates/docs/04-critical-rules/03-scroll-navigation.md +37 -0
- package/templates/docs/04-critical-rules/04-logo-branding.md +42 -0
- package/templates/docs/04-critical-rules/05-troubleshooting.md +46 -0
- package/templates/docs/04-critical-rules/README.md +29 -0
- package/templates/docs/index.md +129 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Survey Page Orchestrator
|
|
2
|
+
|
|
3
|
+
> **Source**: `apps/client/src/components/SurveyPage.tsx`
|
|
4
|
+
> **Role**: The main entry point that wires the UI to the `useSurveySDK` hook.
|
|
5
|
+
|
|
6
|
+
## Setup & Wiring
|
|
7
|
+
|
|
8
|
+
The component must pass the `options` to `useSurveySDK`, including the **instanceId** provided by the client prompt.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' }; // Must be outside component!
|
|
12
|
+
|
|
13
|
+
export default function SurveyPage() {
|
|
14
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>("");
|
|
15
|
+
|
|
16
|
+
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
17
|
+
options: {
|
|
18
|
+
instanceId: 'YOUR_INSTANCE_ID_HERE', // From client prompt
|
|
19
|
+
language: selectedLanguage,
|
|
20
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
21
|
+
debug: true,
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const survey = surveyQueryResults.data;
|
|
26
|
+
|
|
27
|
+
// ... render states
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The 5 Render States
|
|
32
|
+
|
|
33
|
+
A complete `SurveyPage` must handle 5 distinct rendering states in this exact order:
|
|
34
|
+
|
|
35
|
+
### 1. Loading State
|
|
36
|
+
```tsx
|
|
37
|
+
if (surveyQueryResults.isLoading) {
|
|
38
|
+
return <LoadingView /> // Header, Footer, and a "Loading..." message
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Error State
|
|
43
|
+
```tsx
|
|
44
|
+
if (surveyQueryResults.error) {
|
|
45
|
+
return <ErrorView error={surveyQueryResults.error.message} />
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Empty State
|
|
50
|
+
```tsx
|
|
51
|
+
if (!survey) {
|
|
52
|
+
return <EmptyView /> // Header, Footer, and a "No survey data" message
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 4. Submitted State
|
|
57
|
+
```tsx
|
|
58
|
+
if (submitSurveyResults.data) {
|
|
59
|
+
return <SubmitSummaryView /> // Header, Footer, and a "Thank you" message
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 5. Active Survey State
|
|
64
|
+
The main render path. Must include:
|
|
65
|
+
1. `Header`
|
|
66
|
+
2. `ProgressBar` (passing `state.progressPercentage`)
|
|
67
|
+
3. `LanguageSelector` (passing `survey.languages`, `survey.language`, `setSelectedLanguage`)
|
|
68
|
+
4. The question list mapping over `state.currentQuestions`
|
|
69
|
+
5. Back / Next buttons
|
|
70
|
+
6. `Footer`
|
|
71
|
+
|
|
72
|
+
## Question Mapping Pattern
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<div className="space-y-12">
|
|
76
|
+
{state.currentQuestions.map(question => (
|
|
77
|
+
<Question
|
|
78
|
+
key={question.id}
|
|
79
|
+
question={question}
|
|
80
|
+
selectedValue={state.values[question.id]}
|
|
81
|
+
validationError={state.validationErrors[question.id]}
|
|
82
|
+
onSelect={value => onAction({
|
|
83
|
+
type: 'CHANGE',
|
|
84
|
+
payload: { questionId: question.id, value }
|
|
85
|
+
})}
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Navigation Buttons Pattern
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
{/* Back Button (hide on first page) */}
|
|
95
|
+
{state.currentPageIndex > 0 && (
|
|
96
|
+
<button onClick={() => onAction({ type: 'PREVIOUS' })}>
|
|
97
|
+
{survey.language?.startsWith('de') ? 'Zurück' : 'Back'}
|
|
98
|
+
</button>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{/* Next/Submit Button */}
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => onAction({ type: 'NEXT' })}
|
|
104
|
+
disabled={submitSurveyResults.isLoading}
|
|
105
|
+
>
|
|
106
|
+
{submitSurveyResults.isLoading
|
|
107
|
+
? (survey.language?.startsWith('de') ? 'Wird gesendet...' : 'Submitting...')
|
|
108
|
+
: state.currentPageIndex < survey.pages.length - 1
|
|
109
|
+
? (survey.language?.startsWith('de') ? 'Weiter' : 'Next')
|
|
110
|
+
: (survey.language?.startsWith('de') ? 'Absenden' : 'Submit')
|
|
111
|
+
}
|
|
112
|
+
</button>
|
|
113
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Question Dispatcher
|
|
2
|
+
|
|
3
|
+
> **Source**: `apps/client/src/components/Question.tsx`
|
|
4
|
+
> **Role**: Wrapper card component that routes to the correct scale component based on question type.
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
type QuestionProps = {
|
|
10
|
+
question: SurveyQuestion;
|
|
11
|
+
selectedValue?: AnswerValue;
|
|
12
|
+
validationError?: string;
|
|
13
|
+
onSelect: (value: AnswerValue) => void;
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
Every question is wrapped in a standard HTML section:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
23
|
+
{/* 1. Question Text (with required asterisk) */}
|
|
24
|
+
<h2 className="text-lg font-medium leading-relaxed text-gray-900">
|
|
25
|
+
<span dangerouslySetInnerHTML={{ __html: question.text }} />
|
|
26
|
+
{question.required && <span className="text-red-500 ml-1">*</span>}
|
|
27
|
+
</h2>
|
|
28
|
+
|
|
29
|
+
{/* 2. Question Description */}
|
|
30
|
+
{question.description && (
|
|
31
|
+
<div className="text-sm text-gray-600 mt-2 leading-relaxed" dangerouslySetInnerHTML={{ __html: question.description }} />
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
{/* 3. The Scale Component (Dispatcher switch statement) */}
|
|
35
|
+
{question.type === 'rating' && <RatingScale ... />}
|
|
36
|
+
{/* ... other types ... */}
|
|
37
|
+
|
|
38
|
+
{/* 4. Validation Error Banner */}
|
|
39
|
+
{validationError && (
|
|
40
|
+
<div className="mt-4 rounded-[4px] border border-[#333333] bg-[#d9d9d9] py-3.5 px-4 text-left">
|
|
41
|
+
<p className="text-[13px] sm:text-sm font-bold text-gray-900 leading-tight">{validationError}</p>
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</section>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**⚠️ CRITICAL**: The wrapper element **MUST** have `id={question.id}` for the scroll-to-error validation to work!
|
|
48
|
+
|
|
49
|
+
## Inline vs Dedicated Components
|
|
50
|
+
|
|
51
|
+
While complex scales (like Matrix, Slider, FileUpload) get their own dedicated files (e.g. `CsatMatrixScale.tsx`), some simpler types are handled directly inline within `Question.tsx` to reduce boilerplate:
|
|
52
|
+
|
|
53
|
+
1. **`radio` (MCQ)**: Inline mapping of `<label>` and `<input type="radio">`
|
|
54
|
+
2. **`text`**: Inline `<textarea>`
|
|
55
|
+
3. **`text_and_media`**: Inline `<video>` or `<img>` tag
|
|
56
|
+
|
|
57
|
+
(See the respective question type docs for the exact rendering guidance for these inline types).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Rating Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `RatingScale`
|
|
4
|
+
> **Handles**: `type: 'rating'` | `type: 'rating_scale'`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
Renders numbered badge arrays (for NPS 0-10) or icon arrays (stars/emojis) for satisfaction scales.
|
|
8
|
+
|
|
9
|
+
## Props Interface
|
|
10
|
+
```typescript
|
|
11
|
+
type RatingScaleProps = {
|
|
12
|
+
questionId: string;
|
|
13
|
+
options: SurveyOption[];
|
|
14
|
+
selectedValue?: string | number | null | any;
|
|
15
|
+
onSelect: (value: number) => void;
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Sub-Variants
|
|
20
|
+
|
|
21
|
+
### 1. Numbered Badges (NPS / 10-point)
|
|
22
|
+
If `option.color` is present, it's an NPS scale.
|
|
23
|
+
- Map over `options`
|
|
24
|
+
- Render rounded `<button>` for each
|
|
25
|
+
- Use `option.color` as the background color
|
|
26
|
+
- If an option is selected (`selectedValue !== undefined`), apply full opacity to the selected button and dim the unselected ones (`opacity-30`).
|
|
27
|
+
|
|
28
|
+
### 2. Star Scale (`question.scaleStyle === 'star'`)
|
|
29
|
+
- Render generic star icons using `CsatStarIcons.filled` and `CsatStarIcons.empty`
|
|
30
|
+
- Use logic: `i < selectedIndex ? filled : empty` (fill all stars up to the selected one).
|
|
31
|
+
|
|
32
|
+
### 3. Emoji Scale (`question.scaleStyle === 'emoji'`)
|
|
33
|
+
- Render emoji face icons
|
|
34
|
+
- Since options length is variable, use `getEmojiForIndex(i, options.length)` to map the position to the correct sentiment face.
|
|
35
|
+
|
|
36
|
+
## Layout
|
|
37
|
+
- Usually rendered as a `flex` row, `justify-between` or `gap-2` depending on container width.
|
|
38
|
+
- Needs to wrap cleanly on mobile viewports.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# CSAT Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `CsatScale`
|
|
4
|
+
> **Handles**: `type: 'csat'`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
Renders satisfaction scales (typically 1-5). Relies heavily on the `buttonType` property to determine the visual rendering style.
|
|
8
|
+
|
|
9
|
+
## Props Interface
|
|
10
|
+
```typescript
|
|
11
|
+
type CsatScaleProps = {
|
|
12
|
+
question: SurveyQuestion & { type: 'csat' };
|
|
13
|
+
selectedValue?: string | number | null;
|
|
14
|
+
onSelect: (value: number | null) => void;
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Rendering by `buttonType`
|
|
19
|
+
|
|
20
|
+
1. **`buttonType === 'emoji'`**
|
|
21
|
+
- Map over options. Use `CsatEmojiMapping[option.value]` for standard 1-5 scales.
|
|
22
|
+
- Or use `getEmojiForIndex` if scale is variable length.
|
|
23
|
+
|
|
24
|
+
2. **`buttonType === 'star'`**
|
|
25
|
+
- Map options. Fill stars up to the selected index using `CsatStarIcons`.
|
|
26
|
+
|
|
27
|
+
3. **`buttonType === 'numbered'`**
|
|
28
|
+
- Simple rounded `<button>` with the numeric value. No traffic-light colors.
|
|
29
|
+
|
|
30
|
+
4. **`buttonType === 'radio'`**
|
|
31
|
+
- Standard radio list.
|
|
32
|
+
|
|
33
|
+
5. **`buttonType === 'graphics'`**
|
|
34
|
+
- Renders a single continuous slider. Uses `<CustomSliderTrack>`.
|
|
35
|
+
|
|
36
|
+
## N/A Option Handling
|
|
37
|
+
If `question.hasNotApplicable` is true:
|
|
38
|
+
- The options array contains an option with `value: null`.
|
|
39
|
+
- Render a distinct button for N/A.
|
|
40
|
+
- Selecting it passes `null` to `onSelect`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# CSAT Matrix Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `CsatMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'matrix'` (CSAT_MATRIX subType) | `type: 'rating_matrix'`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
Renders a grid where rows are statements and columns are rating/satisfaction levels.
|
|
8
|
+
|
|
9
|
+
## Props Interface
|
|
10
|
+
```typescript
|
|
11
|
+
type CsatMatrixScaleProps = {
|
|
12
|
+
question: SurveyQuestion & { type: 'matrix' | 'rating_matrix' };
|
|
13
|
+
selectedValue: MatrixAnswerMap;
|
|
14
|
+
onSelect: (value: MatrixAnswerMap) => void;
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Structure
|
|
19
|
+
- Renders an HTML `<table>`.
|
|
20
|
+
- **`<thead>`**:
|
|
21
|
+
- Empty top-left cell.
|
|
22
|
+
- Column headers mapped from `question.columns`. Use `col.label`.
|
|
23
|
+
- **`<tbody>`**:
|
|
24
|
+
- Map over `question.rows`.
|
|
25
|
+
- First cell is the row text.
|
|
26
|
+
- Subsequent cells are the input controls mapped from `question.columns`.
|
|
27
|
+
|
|
28
|
+
## Input Control Selection
|
|
29
|
+
Within each table cell, the control rendered depends on `question.buttonType` (similar to CsatScale):
|
|
30
|
+
|
|
31
|
+
- `'emoji'`: Use `getEmojiForIndex(colIndex, cols.length)`
|
|
32
|
+
- `'star'`: Use `CsatStarIcons`
|
|
33
|
+
- `'radio'`: `<input type="radio">` + custom CSS dot
|
|
34
|
+
- `'numbered'`: Badge button (if `rating_matrix`, apply `col.color`)
|
|
35
|
+
|
|
36
|
+
If `buttonType === 'dropdown'`, the table structure is **abandoned**. Instead, it renders a list of rows, each containing a `<MatrixDropdown>` component.
|
|
37
|
+
|
|
38
|
+
## `MatrixAnswerMap` Updates
|
|
39
|
+
When a cell is clicked:
|
|
40
|
+
```tsx
|
|
41
|
+
const newValue = { ...selectedValue, [row.id]: col.value };
|
|
42
|
+
onSelect(newValue);
|
|
43
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Likert Matrix Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `LikertMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'matrix'` (CFM_MATRIX subType)
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
Renders a grid with text-label columns (Likert scale).
|
|
8
|
+
|
|
9
|
+
## Props Interface
|
|
10
|
+
```typescript
|
|
11
|
+
type LikertMatrixScaleProps = {
|
|
12
|
+
question: SurveyQuestion & { type: 'matrix' };
|
|
13
|
+
selectedValue: MatrixAnswerMap;
|
|
14
|
+
onSelect: (value: MatrixAnswerMap) => void;
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Structure & Formats
|
|
19
|
+
|
|
20
|
+
The visual representation changes drastically based on `question.matrixFormat`:
|
|
21
|
+
|
|
22
|
+
### 1. `matrixFormat === 'standard'` (Default)
|
|
23
|
+
- Table grid.
|
|
24
|
+
- Columns are simple radio buttons (`<input type="radio">`).
|
|
25
|
+
- **No emojis or stars** — standard Likert matrices are purely structural text.
|
|
26
|
+
- If `matrixType === 'bipolar'`, the first cell has `row.text` and the last cell has `row.rightText` added.
|
|
27
|
+
|
|
28
|
+
### 2. `matrixFormat === 'carousel'`
|
|
29
|
+
- Shows one row at a time.
|
|
30
|
+
- Renders `row.text` prominently.
|
|
31
|
+
- Renders a standard MCQ-style list of options (`columns`) below it.
|
|
32
|
+
- Has internal state for `currentRowIndex`.
|
|
33
|
+
- Next/Prev buttons step through the rows.
|
|
34
|
+
|
|
35
|
+
### 3. `matrixFormat === 'dropdown'`
|
|
36
|
+
- Renders a list of rows.
|
|
37
|
+
- Each row contains a `<select>` dropdown populated with `question.columns`.
|
|
38
|
+
- Or uses a custom `<MatrixDropdown>` component for better styling.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Slider Matrix Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `SliderMatrixScale`
|
|
4
|
+
> **Handles**: `type: 'slider_matrix'`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
Renders a list of rows, where each row contains an independent range slider.
|
|
8
|
+
|
|
9
|
+
## Props Interface
|
|
10
|
+
```typescript
|
|
11
|
+
type SliderMatrixScaleProps = {
|
|
12
|
+
question: SurveyQuestion & { type: 'slider_matrix' };
|
|
13
|
+
selectedValue: MatrixAnswerMap;
|
|
14
|
+
onSelect: (value: MatrixAnswerMap) => void;
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Structure
|
|
19
|
+
- Not a `<table>`. Rendered as a flex/grid list.
|
|
20
|
+
- Map over `question.rows`.
|
|
21
|
+
- Each row uses the row's specific `min`, `max`, and `step` values.
|
|
22
|
+
- The slider is bound to `selectedValue[row.id]`.
|
|
23
|
+
|
|
24
|
+
## Rendering the Slider
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<input
|
|
28
|
+
type="range"
|
|
29
|
+
min={row.min}
|
|
30
|
+
max={row.max}
|
|
31
|
+
step={row.step}
|
|
32
|
+
value={selectedValue[row.id] ?? row.min}
|
|
33
|
+
onChange={e => onSelect({ ...selectedValue, [row.id]: Number(e.target.value) })}
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If `question.sliderType === 'graphics'`, delegate to `<CustomSliderTrack>` instead of native `<input>`.
|
|
38
|
+
|
|
39
|
+
If `question.enableNotApplicable` is true, render a checkbox next to each slider:
|
|
40
|
+
```tsx
|
|
41
|
+
<input type="checkbox" onChange={e => {
|
|
42
|
+
onSelect({ ...selectedValue, [row.id]: e.target.checked ? null : row.min })
|
|
43
|
+
}} />
|
|
44
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# File Upload Scale Component
|
|
2
|
+
|
|
3
|
+
> **Component**: `FileUploadScale`
|
|
4
|
+
> **Handles**: `type: 'file_upload'`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
Provides a UI for selecting files, enforcing limits, and displaying the selected list.
|
|
8
|
+
|
|
9
|
+
## Props Interface
|
|
10
|
+
```typescript
|
|
11
|
+
type FileUploadScaleProps = {
|
|
12
|
+
question: SurveyQuestion & { type: 'file_upload' };
|
|
13
|
+
selectedValue?: any[]; // Expected to be UploadedFile[]
|
|
14
|
+
onSelect: (value: any[]) => void;
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Structure
|
|
19
|
+
1. **Dropzone / Browse Button**
|
|
20
|
+
- `<input type="file" multiple={maxFileCount > 1} className="hidden">`
|
|
21
|
+
- Visible button triggers `.click()` on hidden input.
|
|
22
|
+
2. **File List**
|
|
23
|
+
- Map over `selectedValue || []`.
|
|
24
|
+
- Show file name and remove button.
|
|
25
|
+
3. **Error Text**
|
|
26
|
+
- Local state for size limit or format errors.
|
|
27
|
+
|
|
28
|
+
## Limit Enforcement
|
|
29
|
+
- `question.maxFileCount`
|
|
30
|
+
- `question.supportedFileFormats` (e.g. `['PDF', 'PNG']`)
|
|
31
|
+
- `question.fileSizeLimit` (MB)
|
|
32
|
+
|
|
33
|
+
When files are selected, validate before adding to `selectedValue`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Custom Slider Track
|
|
2
|
+
|
|
3
|
+
> **Component**: `CustomSliderTrack`
|
|
4
|
+
> **Used by**: `SliderMatrixScale`, `CsatMatrixScale`, `CsatScale`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
A shared presentation component that overrides native browser range slider styling to provide an emoji-thumb or branded track.
|
|
8
|
+
|
|
9
|
+
## Why it exists
|
|
10
|
+
Native `<input type="range">` cannot reliably render an emoji inside the draggable thumb handle cross-browser. This component uses CSS tricks (often a transparent native range input layered over a custom styled `div` track) to achieve the design.
|
|
11
|
+
|
|
12
|
+
## Props Interface
|
|
13
|
+
```typescript
|
|
14
|
+
type CustomSliderTrackProps = {
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
step: number;
|
|
18
|
+
value: number;
|
|
19
|
+
onChange: (val: number) => void;
|
|
20
|
+
sliderType?: 'graphics' | 'standard';
|
|
21
|
+
// Additional props for tick marks, colors
|
|
22
|
+
};
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Emoji Thumb Logic
|
|
26
|
+
If `sliderType === 'graphics'`, the thumb handle should dynamically display an emoji corresponding to the current value.
|
|
27
|
+
It uses `getEmojiForIndex` from `icons.tsx`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Header, Footer & Branding
|
|
2
|
+
|
|
3
|
+
> **Components**: `Header`, `Footer`
|
|
4
|
+
> **Role**: Branding shells around the survey content.
|
|
5
|
+
|
|
6
|
+
## Strict Logo Requirement
|
|
7
|
+
|
|
8
|
+
**⚠️ CRITICAL RULE**: You must NOT use `<img>` tags pointing to external Telekom or Sprinklr URLs. You must NOT download static images into the `public/` folder.
|
|
9
|
+
|
|
10
|
+
All logos must be constructed **purely using Tailwind CSS** geometric shapes.
|
|
11
|
+
|
|
12
|
+
## The Header (Abstract Brand)
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
export default function Header() {
|
|
16
|
+
return (
|
|
17
|
+
<header className="flex h-16 items-center justify-between border-b px-6">
|
|
18
|
+
{/* CSS-only Abstract Logo */}
|
|
19
|
+
<div className="flex items-center gap-2">
|
|
20
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[#e20074]">
|
|
21
|
+
<div className="h-4 w-4 rounded-full bg-white" />
|
|
22
|
+
</div>
|
|
23
|
+
<span className="text-xl font-bold tracking-tight text-gray-900">
|
|
24
|
+
YourBrand
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
</header>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The Footer
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
export default function Footer() {
|
|
36
|
+
return (
|
|
37
|
+
<footer className="border-t py-6 text-center text-sm text-gray-500">
|
|
38
|
+
<p>Powered by Sprinklr CFM SDK</p>
|
|
39
|
+
</footer>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Customization
|
|
45
|
+
|
|
46
|
+
The client may provide a prompt asking for specific colors ("make it dark mode", "use blue"). Update the CSS classes in `SurveyPage`, `Header`, and `Footer` to match their request. Do not try to reverse-engineer their brand logo exactly — stick to clean abstract CSS geometry unless you have an explicit SVG path.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Progress Bar
|
|
2
|
+
|
|
3
|
+
> **Component**: `ProgressBar`
|
|
4
|
+
> **Role**: Displays completion status at the top of the survey.
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type ProgressBarProps = {
|
|
9
|
+
progressPercentage: number; // 0 to 100
|
|
10
|
+
};
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Implementation
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
export default function ProgressBar({ progressPercentage }: ProgressBarProps) {
|
|
17
|
+
// Ensure it's bounded between 0 and 100
|
|
18
|
+
const width = Math.min(100, Math.max(0, progressPercentage));
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="mb-8 h-2 w-full overflow-hidden rounded-full bg-gray-100">
|
|
22
|
+
<div
|
|
23
|
+
className="h-full bg-[#e20074] transition-all duration-500 ease-out"
|
|
24
|
+
style={{ width: `${width}%` }}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The `progressPercentage` value is provided directly by `state.progressPercentage` from the `useSurveySDK` hook.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Language Selector
|
|
2
|
+
|
|
3
|
+
> **Component**: `LanguageSelector`
|
|
4
|
+
> **Role**: Allows the user to switch the survey language.
|
|
5
|
+
|
|
6
|
+
## Props Interface
|
|
7
|
+
```typescript
|
|
8
|
+
type LanguageSelectorProps = {
|
|
9
|
+
languages: SurveyLanguage[];
|
|
10
|
+
selectedLanguage: string;
|
|
11
|
+
onChange: (code: string) => void;
|
|
12
|
+
};
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
Only render the selector if there is more than 1 language available.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
export default function LanguageSelector({ languages, selectedLanguage, onChange }: LanguageSelectorProps) {
|
|
21
|
+
if (!languages || languages.length <= 1) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="mb-6 flex justify-end">
|
|
25
|
+
<select
|
|
26
|
+
value={selectedLanguage}
|
|
27
|
+
onChange={e => onChange(e.target.value)}
|
|
28
|
+
className="rounded-md border border-gray-300 py-1.5 pl-3 pr-8 text-sm focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]"
|
|
29
|
+
>
|
|
30
|
+
{languages.map(lang => (
|
|
31
|
+
<option key={lang.code} value={lang.code}>
|
|
32
|
+
{lang.name}
|
|
33
|
+
</option>
|
|
34
|
+
))}
|
|
35
|
+
</select>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When `onChange` fires, it updates state in `SurveyPage.tsx`, which triggers the SDK to re-map the translations.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Matrix Dropdown
|
|
2
|
+
|
|
3
|
+
> **Component**: `MatrixDropdown`
|
|
4
|
+
> **Used by**: `CsatMatrixScale`, `LikertMatrixScale`
|
|
5
|
+
|
|
6
|
+
## Role
|
|
7
|
+
Renders a dropdown selector for a single row in a matrix question. Used when `matrixFormat === 'dropdown'` or `buttonType === 'dropdown'`.
|
|
8
|
+
|
|
9
|
+
## Props Interface
|
|
10
|
+
```typescript
|
|
11
|
+
type MatrixDropdownProps = {
|
|
12
|
+
columns: MatrixColumn[];
|
|
13
|
+
selectedValue: string | number | null | undefined;
|
|
14
|
+
onChange: (value: string | number | null) => void;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
Renders a native HTML `<select>` element.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<select
|
|
24
|
+
value={selectedValue === null ? '' : String(selectedValue ?? '')}
|
|
25
|
+
onChange={e => {
|
|
26
|
+
const val = e.target.value;
|
|
27
|
+
if (val === '') onChange(null); // Map empty string back to null for N/A
|
|
28
|
+
else {
|
|
29
|
+
// Find original option to keep type (number vs string) intact
|
|
30
|
+
const opt = columns.find(c => String(c.value) === val);
|
|
31
|
+
onChange(opt ? opt.value : val);
|
|
32
|
+
}
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<option value="" disabled>Select an option...</option>
|
|
36
|
+
{columns.map(col => (
|
|
37
|
+
<option key={String(col.value)} value={String(col.value)}>
|
|
38
|
+
{col.label}
|
|
39
|
+
</option>
|
|
40
|
+
))}
|
|
41
|
+
</select>
|
|
42
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Client Components Overview
|
|
2
|
+
|
|
3
|
+
> This folder documents the React client components that render the survey UI.
|
|
4
|
+
> It explains the architecture, the dispatcher (`Question.tsx`), and all 11 individual question scale components.
|
|
5
|
+
|
|
6
|
+
## Component Architecture
|
|
7
|
+
|
|
8
|
+
The UI is driven by a single top-level `SurveyPage` component which iterates over the `currentQuestions` array and passes each `SurveyQuestion` object to the `Question` dispatcher component.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
SurveyPage (Top-level orchestrator)
|
|
12
|
+
│
|
|
13
|
+
├── Header (Branding/Logo)
|
|
14
|
+
├── ProgressBar
|
|
15
|
+
├── LanguageSelector
|
|
16
|
+
├── Footer
|
|
17
|
+
│
|
|
18
|
+
└── Question (Dispatcher, mapped over `currentQuestions`)
|
|
19
|
+
│
|
|
20
|
+
├── RatingScale (handles 'rating' and 'rating_scale')
|
|
21
|
+
├── CsatScale (handles 'csat')
|
|
22
|
+
├── LikertMatrixScale (handles 'matrix' CFM_MATRIX)
|
|
23
|
+
├── CsatMatrixScale (handles 'matrix' CSAT_MATRIX / 'rating_matrix')
|
|
24
|
+
├── SliderMatrixScale (handles 'slider_matrix')
|
|
25
|
+
├── FileUploadScale (handles 'file_upload')
|
|
26
|
+
│
|
|
27
|
+
├── (Inline UI for 'radio', 'text', 'text_and_media')
|
|
28
|
+
│
|
|
29
|
+
└── Shared Sub-Components:
|
|
30
|
+
├── CustomSliderTrack (used by csat/matrix sliders)
|
|
31
|
+
└── MatrixDropdown (used by matrix dropdowns)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Styling Approach
|
|
35
|
+
|
|
36
|
+
- Built entirely with **Tailwind CSS**.
|
|
37
|
+
- **No external UI libraries** (no Material UI, Chakra, etc.).
|
|
38
|
+
- The default brand colour is Telekom Magenta: `#e20074`.
|
|
39
|
+
- The UI follows a strict, clean, modern corporate aesthetic (card-based layout, subtle borders, pink accents on selection).
|
|
40
|
+
|
|
41
|
+
## The `Question` Dispatcher
|
|
42
|
+
|
|
43
|
+
The `Question` component (`apps/client/src/components/Question.tsx`) is the heart of the rendering engine. It receives a `SurveyQuestion` union type and switches on `question.type` (and `question.subType` for matrices) to render the correct specialized scale component.
|
|
44
|
+
|
|
45
|
+
See `02-question.md` for details on the dispatcher logic.
|
|
46
|
+
|
|
47
|
+
## Files in This Folder
|
|
48
|
+
|
|
49
|
+
| File | Component |
|
|
50
|
+
|------|-----------|
|
|
51
|
+
| `01-survey-page.md` | `SurveyPage` orchestrator |
|
|
52
|
+
| `02-question.md` | `Question` dispatcher & inline types |
|
|
53
|
+
| `03-rating-scale.md` | `RatingScale` (NPS/Stars) |
|
|
54
|
+
| `04-csat-scale.md` | `CsatScale` (Emoji/Star CSAT) |
|
|
55
|
+
| `05-csat-matrix-scale.md` | `CsatMatrixScale` (Satisfaction Grid) |
|
|
56
|
+
| `06-likert-matrix-scale.md` | `LikertMatrixScale` (Text Grid) |
|
|
57
|
+
| `07-slider-matrix-scale.md` | `SliderMatrixScale` (Sliders Grid) |
|
|
58
|
+
| `08-file-upload-scale.md` | `FileUploadScale` |
|
|
59
|
+
| `09-custom-slider-track.md` | `CustomSliderTrack` (Emoji thumbs) |
|
|
60
|
+
| `10-header-footer.md` | `Header` & `Footer` |
|
|
61
|
+
| `11-progress-bar.md` | `ProgressBar` |
|
|
62
|
+
| `12-language-selector.md` | `LanguageSelector` |
|
|
63
|
+
| `13-matrix-dropdown.md` | `MatrixDropdown` |
|