@tantainnovative/ndpr-toolkit 1.0.2 → 1.0.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.
Files changed (212) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/.eslintrc.json +10 -0
  3. package/.github/workflows/ci.yml +36 -0
  4. package/.github/workflows/nextjs.yml +104 -0
  5. package/.husky/commit-msg +4 -0
  6. package/.husky/pre-commit +4 -0
  7. package/.lintstagedrc.js +4 -0
  8. package/.nvmrc +1 -0
  9. package/.versionrc +17 -0
  10. package/CHANGELOG.md +16 -0
  11. package/CLAUDE.md +90 -0
  12. package/CNAME +1 -0
  13. package/CONTRIBUTING.md +87 -0
  14. package/README.md +84 -447
  15. package/RELEASE-NOTES-v1.0.0.md +140 -0
  16. package/RELEASE-NOTES-v1.0.1.md +69 -0
  17. package/SECURITY.md +21 -0
  18. package/commitlint.config.js +36 -0
  19. package/components.json +21 -0
  20. package/eslint.config.mjs +16 -0
  21. package/jest.config.js +31 -0
  22. package/jest.setup.js +15 -0
  23. package/next.config.js +15 -0
  24. package/next.config.ts +62 -0
  25. package/package.json +70 -52
  26. package/packages/ndpr-toolkit/README.md +467 -0
  27. package/packages/ndpr-toolkit/jest.config.js +23 -0
  28. package/packages/ndpr-toolkit/package-lock.json +8197 -0
  29. package/packages/ndpr-toolkit/package.json +71 -0
  30. package/packages/ndpr-toolkit/rollup.config.js +34 -0
  31. package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentBanner.test.tsx +119 -0
  32. package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentManager.test.tsx +122 -0
  33. package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentStorage.test.tsx +270 -0
  34. package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRDashboard.test.tsx +199 -0
  35. package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRRequestForm.test.tsx +224 -0
  36. package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRTracker.test.tsx +104 -0
  37. package/packages/ndpr-toolkit/src/__tests__/hooks/useConsent.test.tsx +161 -0
  38. package/packages/ndpr-toolkit/src/__tests__/hooks/useDSR.test.tsx +330 -0
  39. package/packages/ndpr-toolkit/src/__tests__/utils/breach.test.ts +149 -0
  40. package/packages/ndpr-toolkit/src/__tests__/utils/consent.test.ts +88 -0
  41. package/packages/ndpr-toolkit/src/__tests__/utils/dpia.test.ts +160 -0
  42. package/packages/ndpr-toolkit/src/__tests__/utils/dsr.test.ts +110 -0
  43. package/packages/ndpr-toolkit/src/__tests__/utils/privacy.test.ts +97 -0
  44. package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
  45. package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
  46. package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
  47. package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
  48. package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
  49. package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
  50. package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
  51. package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
  52. package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
  53. package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
  54. package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
  55. package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
  56. package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
  57. package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
  58. package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
  59. package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
  60. package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
  61. package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
  62. package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
  63. package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
  64. package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
  65. package/{dist/index.d.ts → packages/ndpr-toolkit/src/index.ts} +13 -0
  66. package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
  67. package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
  68. package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
  69. package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
  70. package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
  71. package/packages/ndpr-toolkit/src/types/index.ts +42 -0
  72. package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
  73. package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
  74. package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
  75. package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
  76. package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
  77. package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
  78. package/packages/ndpr-toolkit/tsconfig.json +23 -0
  79. package/postcss.config.mjs +5 -0
  80. package/public/NDPR TOOLKIT.svg +1 -0
  81. package/public/favicon/android-chrome-192x192.png +0 -0
  82. package/public/favicon/android-chrome-512x512.png +0 -0
  83. package/public/favicon/apple-touch-icon.png +0 -0
  84. package/public/favicon/favicon-16x16.png +0 -0
  85. package/public/favicon/favicon-32x32.png +0 -0
  86. package/public/favicon/site.webmanifest +1 -0
  87. package/public/file.svg +1 -0
  88. package/public/globe.svg +1 -0
  89. package/public/ndpr-toolkit-logo.svg +108 -0
  90. package/public/next.svg +1 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/src/__tests__/example.test.ts +13 -0
  94. package/src/__tests__/requestService.test.ts +57 -0
  95. package/src/app/accessibility.css +70 -0
  96. package/src/app/docs/components/DocLayout.tsx +267 -0
  97. package/src/app/docs/components/breach-notification/page.tsx +797 -0
  98. package/src/app/docs/components/consent-management/page.tsx +576 -0
  99. package/src/app/docs/components/data-subject-rights/page.tsx +511 -0
  100. package/src/app/docs/components/dpia-questionnaire/layout.tsx +15 -0
  101. package/src/app/docs/components/dpia-questionnaire/metadata.ts +31 -0
  102. package/src/app/docs/components/dpia-questionnaire/page.tsx +666 -0
  103. package/src/app/docs/components/hooks/page.tsx +305 -0
  104. package/src/app/docs/components/page.tsx +84 -0
  105. package/src/app/docs/components/privacy-policy-generator/page.tsx +634 -0
  106. package/src/app/docs/guides/breach-notification-process/components/BestPractices.tsx +123 -0
  107. package/src/app/docs/guides/breach-notification-process/components/ImplementationSteps.tsx +328 -0
  108. package/src/app/docs/guides/breach-notification-process/components/Introduction.tsx +28 -0
  109. package/src/app/docs/guides/breach-notification-process/components/NotificationTimeline.tsx +91 -0
  110. package/src/app/docs/guides/breach-notification-process/components/Resources.tsx +118 -0
  111. package/src/app/docs/guides/breach-notification-process/page.tsx +39 -0
  112. package/src/app/docs/guides/conducting-dpia/page.tsx +593 -0
  113. package/src/app/docs/guides/data-subject-requests/page.tsx +666 -0
  114. package/src/app/docs/guides/managing-consent/page.tsx +738 -0
  115. package/src/app/docs/guides/ndpr-compliance-checklist/components/ComplianceChecklist.tsx +296 -0
  116. package/src/app/docs/guides/ndpr-compliance-checklist/components/ImplementationTools.tsx +145 -0
  117. package/src/app/docs/guides/ndpr-compliance-checklist/components/Introduction.tsx +33 -0
  118. package/src/app/docs/guides/ndpr-compliance-checklist/components/KeyRequirements.tsx +99 -0
  119. package/src/app/docs/guides/ndpr-compliance-checklist/components/Resources.tsx +159 -0
  120. package/src/app/docs/guides/ndpr-compliance-checklist/page.tsx +38 -0
  121. package/src/app/docs/guides/page.tsx +67 -0
  122. package/src/app/docs/layout.tsx +15 -0
  123. package/src/app/docs/metadata.ts +31 -0
  124. package/src/app/docs/page.tsx +572 -0
  125. package/src/app/favicon.ico +0 -0
  126. package/src/app/globals.css +123 -0
  127. package/src/app/layout.tsx +37 -0
  128. package/src/app/ndpr-demos/breach/page.tsx +354 -0
  129. package/src/app/ndpr-demos/consent/page.tsx +366 -0
  130. package/src/app/ndpr-demos/dpia/page.tsx +495 -0
  131. package/src/app/ndpr-demos/dsr/page.tsx +280 -0
  132. package/src/app/ndpr-demos/page.tsx +73 -0
  133. package/src/app/ndpr-demos/policy/page.tsx +771 -0
  134. package/src/app/page.tsx +452 -0
  135. package/src/components/ErrorBoundary.tsx +90 -0
  136. package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
  137. package/src/components/consent/ConsentBanner.tsx +159 -0
  138. package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +419 -0
  139. package/src/components/docs/DocLayout.tsx +289 -0
  140. package/src/components/docs/index.ts +2 -0
  141. package/src/components/dpia/DPIAQuestionnaire.tsx +483 -0
  142. package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
  143. package/src/components/privacy-policy/data.ts +98 -0
  144. package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
  145. package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
  146. package/src/components/privacy-policy/shared/FormField.tsx +79 -0
  147. package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
  148. package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +335 -0
  149. package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
  150. package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
  151. package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
  152. package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +172 -0
  153. package/src/components/ui/Badge.tsx +46 -0
  154. package/src/components/ui/Button.tsx +59 -0
  155. package/src/components/ui/Card.tsx +92 -0
  156. package/src/components/ui/Checkbox.tsx +57 -0
  157. package/src/components/ui/FormField.tsx +50 -0
  158. package/src/components/ui/Input.tsx +38 -0
  159. package/src/components/ui/Loading.tsx +201 -0
  160. package/src/components/ui/Select.tsx +42 -0
  161. package/src/components/ui/TextArea.tsx +38 -0
  162. package/src/components/ui/label.tsx +24 -0
  163. package/src/components/ui/switch.tsx +31 -0
  164. package/src/components/ui/tabs.tsx +66 -0
  165. package/src/hooks/useConsent.ts +64 -0
  166. package/src/hooks/useLoadingState.ts +85 -0
  167. package/src/lib/consentService.ts +137 -0
  168. package/src/lib/dpiaQuestions.ts +148 -0
  169. package/src/lib/requestService.ts +75 -0
  170. package/src/lib/sanitize.ts +108 -0
  171. package/src/lib/storage.ts +222 -0
  172. package/src/lib/utils.ts +6 -0
  173. package/src/types/html-to-docx.d.ts +30 -0
  174. package/src/types/index.ts +72 -0
  175. package/tailwind.config.ts +65 -0
  176. package/tsconfig.json +41 -0
  177. package/dist/components/breach/BreachNotificationManager.d.ts +0 -62
  178. package/dist/components/breach/BreachReportForm.d.ts +0 -66
  179. package/dist/components/breach/BreachRiskAssessment.d.ts +0 -50
  180. package/dist/components/breach/RegulatoryReportGenerator.d.ts +0 -94
  181. package/dist/components/consent/ConsentBanner.d.ts +0 -79
  182. package/dist/components/consent/ConsentManager.d.ts +0 -73
  183. package/dist/components/consent/ConsentStorage.d.ts +0 -41
  184. package/dist/components/dpia/DPIAQuestionnaire.d.ts +0 -70
  185. package/dist/components/dpia/DPIAReport.d.ts +0 -40
  186. package/dist/components/dpia/StepIndicator.d.ts +0 -64
  187. package/dist/components/dsr/DSRDashboard.d.ts +0 -58
  188. package/dist/components/dsr/DSRRequestForm.d.ts +0 -74
  189. package/dist/components/dsr/DSRTracker.d.ts +0 -56
  190. package/dist/components/policy/PolicyExporter.d.ts +0 -65
  191. package/dist/components/policy/PolicyGenerator.d.ts +0 -54
  192. package/dist/components/policy/PolicyPreview.d.ts +0 -71
  193. package/dist/hooks/useBreach.d.ts +0 -97
  194. package/dist/hooks/useConsent.d.ts +0 -63
  195. package/dist/hooks/useDPIA.d.ts +0 -92
  196. package/dist/hooks/useDSR.d.ts +0 -72
  197. package/dist/hooks/usePrivacyPolicy.d.ts +0 -87
  198. package/dist/index.esm.js +0 -2
  199. package/dist/index.esm.js.map +0 -1
  200. package/dist/index.js +0 -2
  201. package/dist/index.js.map +0 -1
  202. package/dist/setupTests.d.ts +0 -2
  203. package/dist/types/breach.d.ts +0 -239
  204. package/dist/types/consent.d.ts +0 -95
  205. package/dist/types/dpia.d.ts +0 -196
  206. package/dist/types/dsr.d.ts +0 -162
  207. package/dist/types/privacy.d.ts +0 -204
  208. package/dist/utils/breach.d.ts +0 -14
  209. package/dist/utils/consent.d.ts +0 -10
  210. package/dist/utils/dpia.d.ts +0 -12
  211. package/dist/utils/dsr.d.ts +0 -11
  212. package/dist/utils/privacy.d.ts +0 -12
@@ -0,0 +1,342 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { DPIASection, DPIAQuestion } from '../../types/dpia';
3
+
4
+ export interface DPIAQuestionnaireProps {
5
+ /**
6
+ * Sections of the DPIA questionnaire
7
+ */
8
+ sections: DPIASection[];
9
+
10
+ /**
11
+ * Current answers to the questionnaire
12
+ */
13
+ answers: Record<string, any>;
14
+
15
+ /**
16
+ * Callback function called when an answer is updated
17
+ */
18
+ onAnswerChange: (questionId: string, value: any) => void;
19
+
20
+ /**
21
+ * Current section index
22
+ */
23
+ currentSectionIndex: number;
24
+
25
+ /**
26
+ * Callback function called when user navigates to the next section
27
+ */
28
+ onNextSection?: () => void;
29
+
30
+ /**
31
+ * Callback function called when user navigates to the previous section
32
+ */
33
+ onPrevSection?: () => void;
34
+
35
+ /**
36
+ * Validation errors for the current section
37
+ */
38
+ validationErrors?: Record<string, string>;
39
+
40
+ /**
41
+ * Whether the questionnaire is in read-only mode
42
+ * @default false
43
+ */
44
+ readOnly?: boolean;
45
+
46
+ /**
47
+ * Custom CSS class for the questionnaire
48
+ */
49
+ className?: string;
50
+
51
+ /**
52
+ * Custom CSS class for the buttons
53
+ */
54
+ buttonClassName?: string;
55
+
56
+ /**
57
+ * Text for the next button
58
+ * @default "Next"
59
+ */
60
+ nextButtonText?: string;
61
+
62
+ /**
63
+ * Text for the previous button
64
+ * @default "Previous"
65
+ */
66
+ prevButtonText?: string;
67
+
68
+ /**
69
+ * Text for the submit button (shown on the last section)
70
+ * @default "Submit"
71
+ */
72
+ submitButtonText?: string;
73
+
74
+ /**
75
+ * Whether to show a progress indicator
76
+ * @default true
77
+ */
78
+ showProgress?: boolean;
79
+
80
+ /**
81
+ * Current progress percentage (0-100)
82
+ */
83
+ progress?: number;
84
+ }
85
+
86
+ export const DPIAQuestionnaire: React.FC<DPIAQuestionnaireProps> = ({
87
+ sections,
88
+ answers,
89
+ onAnswerChange,
90
+ currentSectionIndex,
91
+ onNextSection,
92
+ onPrevSection,
93
+ validationErrors = {},
94
+ readOnly = false,
95
+ className = "",
96
+ buttonClassName = "",
97
+ nextButtonText = "Next",
98
+ prevButtonText = "Previous",
99
+ submitButtonText = "Submit",
100
+ showProgress = true,
101
+ progress
102
+ }) => {
103
+ const currentSection = sections[currentSectionIndex];
104
+ const isLastSection = currentSectionIndex === sections.length - 1;
105
+
106
+ // Check if a question should be shown based on its conditions
107
+ const shouldShowQuestion = (question: DPIAQuestion): boolean => {
108
+ if (!question.showWhen) {
109
+ return true;
110
+ }
111
+
112
+ return question.showWhen.every(condition => {
113
+ const answer = answers[condition.questionId];
114
+
115
+ switch (condition.operator) {
116
+ case 'equals':
117
+ return answer === condition.value;
118
+ case 'contains':
119
+ return Array.isArray(answer) ? answer.includes(condition.value) : false;
120
+ case 'greaterThan':
121
+ return typeof answer === 'number' ? answer > condition.value : false;
122
+ case 'lessThan':
123
+ return typeof answer === 'number' ? answer < condition.value : false;
124
+ default:
125
+ return true;
126
+ }
127
+ });
128
+ };
129
+
130
+ // Render a question based on its type
131
+ const renderQuestion = (question: DPIAQuestion) => {
132
+ if (!shouldShowQuestion(question)) {
133
+ return null;
134
+ }
135
+
136
+ const error = validationErrors[question.id];
137
+ const value = answers[question.id];
138
+
139
+ return (
140
+ <div key={question.id} className="mb-6">
141
+ <div className="mb-2">
142
+ <label htmlFor={question.id} className="block text-sm font-medium text-gray-900 dark:text-gray-100">
143
+ {question.text}
144
+ {question.required && <span className="text-red-500 ml-1">*</span>}
145
+ </label>
146
+ {question.guidance && (
147
+ <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{question.guidance}</p>
148
+ )}
149
+ </div>
150
+
151
+ {question.type === 'text' && (
152
+ <input
153
+ type="text"
154
+ id={question.id}
155
+ value={value || ''}
156
+ onChange={e => onAnswerChange(question.id, e.target.value)}
157
+ disabled={readOnly}
158
+ className={`w-full px-3 py-2 border rounded-md ${
159
+ error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
160
+ } focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`}
161
+ />
162
+ )}
163
+
164
+ {question.type === 'textarea' && (
165
+ <textarea
166
+ id={question.id}
167
+ value={value || ''}
168
+ onChange={e => onAnswerChange(question.id, e.target.value)}
169
+ disabled={readOnly}
170
+ rows={4}
171
+ className={`w-full px-3 py-2 border rounded-md ${
172
+ error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
173
+ } focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`}
174
+ />
175
+ )}
176
+
177
+ {question.type === 'select' && question.options && (
178
+ <select
179
+ id={question.id}
180
+ value={value || ''}
181
+ onChange={e => onAnswerChange(question.id, e.target.value)}
182
+ disabled={readOnly}
183
+ className={`w-full px-3 py-2 border rounded-md ${
184
+ error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
185
+ } focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`}
186
+ >
187
+ <option value="">Select an option</option>
188
+ {question.options.map(option => (
189
+ <option key={option.value} value={option.value}>
190
+ {option.label}
191
+ </option>
192
+ ))}
193
+ </select>
194
+ )}
195
+
196
+ {question.type === 'radio' && question.options && (
197
+ <div className="space-y-2">
198
+ {question.options.map(option => (
199
+ <div key={option.value} className="flex items-center">
200
+ <input
201
+ type="radio"
202
+ id={`${question.id}_${option.value}`}
203
+ name={question.id}
204
+ value={option.value}
205
+ checked={value === option.value}
206
+ onChange={() => onAnswerChange(question.id, option.value)}
207
+ disabled={readOnly}
208
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600"
209
+ />
210
+ <label
211
+ htmlFor={`${question.id}_${option.value}`}
212
+ className="ml-2 block text-sm text-gray-900 dark:text-gray-100"
213
+ >
214
+ {option.label}
215
+ {option.riskLevel && (
216
+ <span className={`ml-2 text-xs px-2 py-1 rounded ${
217
+ option.riskLevel === 'low' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
218
+ option.riskLevel === 'medium' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
219
+ 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
220
+ }`}>
221
+ {option.riskLevel.charAt(0).toUpperCase() + option.riskLevel.slice(1)} Risk
222
+ </span>
223
+ )}
224
+ </label>
225
+ </div>
226
+ ))}
227
+ </div>
228
+ )}
229
+
230
+ {question.type === 'checkbox' && question.options && (
231
+ <div className="space-y-2">
232
+ {question.options.map(option => (
233
+ <div key={option.value} className="flex items-center">
234
+ <input
235
+ type="checkbox"
236
+ id={`${question.id}_${option.value}`}
237
+ value={option.value}
238
+ checked={Array.isArray(value) ? value.includes(option.value) : false}
239
+ onChange={e => {
240
+ const currentValues = Array.isArray(value) ? [...value] : [];
241
+ if (e.target.checked) {
242
+ onAnswerChange(question.id, [...currentValues, option.value]);
243
+ } else {
244
+ onAnswerChange(question.id, currentValues.filter(v => v !== option.value));
245
+ }
246
+ }}
247
+ disabled={readOnly}
248
+ className="h-4 w-4 rounded text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600"
249
+ />
250
+ <label
251
+ htmlFor={`${question.id}_${option.value}`}
252
+ className="ml-2 block text-sm text-gray-900 dark:text-gray-100"
253
+ >
254
+ {option.label}
255
+ </label>
256
+ </div>
257
+ ))}
258
+ </div>
259
+ )}
260
+
261
+ {question.type === 'scale' && (
262
+ <div>
263
+ <div className="flex justify-between mb-2">
264
+ {question.scaleLabels && Object.entries(question.scaleLabels).map(([scaleValue, label]) => (
265
+ <div key={scaleValue} className="text-xs text-gray-500 dark:text-gray-400 text-center" style={{ width: `${100 / Object.keys(question.scaleLabels || {}).length}%` }}>
266
+ {label}
267
+ </div>
268
+ ))}
269
+ </div>
270
+ <input
271
+ type="range"
272
+ id={question.id}
273
+ min={question.minValue || 1}
274
+ max={question.maxValue || 5}
275
+ value={value || (question.minValue || 1)}
276
+ onChange={e => onAnswerChange(question.id, parseInt(e.target.value, 10))}
277
+ disabled={readOnly}
278
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
279
+ />
280
+ <div className="mt-1 text-sm text-gray-500 dark:text-gray-400 text-center">
281
+ Selected value: {value || (question.minValue || 1)}
282
+ </div>
283
+ </div>
284
+ )}
285
+
286
+ {error && <p className="mt-1 text-sm text-red-500">{error}</p>}
287
+ </div>
288
+ );
289
+ };
290
+
291
+ if (!currentSection) {
292
+ return <div>No section found.</div>;
293
+ }
294
+
295
+ return (
296
+ <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 ${className}`}>
297
+ {showProgress && (
298
+ <div className="mb-6">
299
+ <div className="flex justify-between text-sm text-gray-500 dark:text-gray-400 mb-1">
300
+ <span>Section {currentSectionIndex + 1} of {sections.length}</span>
301
+ <span>{progress !== undefined ? `${progress}% Complete` : ''}</span>
302
+ </div>
303
+ <div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
304
+ <div
305
+ className="bg-blue-600 h-2.5 rounded-full"
306
+ style={{ width: `${progress !== undefined ? progress : ((currentSectionIndex + 1) / sections.length) * 100}%` }}
307
+ ></div>
308
+ </div>
309
+ </div>
310
+ )}
311
+
312
+ <h2 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">{currentSection.title}</h2>
313
+ {currentSection.description && (
314
+ <p className="mb-6 text-gray-600 dark:text-gray-300">{currentSection.description}</p>
315
+ )}
316
+
317
+ <div className="space-y-6">
318
+ {currentSection.questions.map(question => renderQuestion(question))}
319
+ </div>
320
+
321
+ <div className="mt-8 flex justify-between">
322
+ <button
323
+ type="button"
324
+ onClick={onPrevSection}
325
+ disabled={currentSectionIndex === 0 || readOnly}
326
+ className={`px-4 py-2 bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-white rounded hover:bg-gray-300 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed ${buttonClassName}`}
327
+ >
328
+ {prevButtonText}
329
+ </button>
330
+
331
+ <button
332
+ type="button"
333
+ onClick={onNextSection}
334
+ disabled={readOnly}
335
+ className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed ${buttonClassName}`}
336
+ >
337
+ {isLastSection ? submitButtonText : nextButtonText}
338
+ </button>
339
+ </div>
340
+ </div>
341
+ );
342
+ };