@tantainnovative/ndpr-toolkit 1.0.4 → 1.0.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.
Files changed (132) hide show
  1. package/eslint.config.mjs +16 -0
  2. package/next.config.js +15 -0
  3. package/next.config.ts +62 -0
  4. package/package.json +1 -1
  5. package/packages/ndpr-toolkit/README.md +467 -0
  6. package/packages/ndpr-toolkit/dist/components/breach/BreachNotificationManager.d.ts +62 -0
  7. package/packages/ndpr-toolkit/dist/components/breach/BreachReportForm.d.ts +66 -0
  8. package/packages/ndpr-toolkit/dist/components/breach/BreachRiskAssessment.d.ts +50 -0
  9. package/packages/ndpr-toolkit/dist/components/breach/RegulatoryReportGenerator.d.ts +94 -0
  10. package/packages/ndpr-toolkit/dist/components/consent/ConsentBanner.d.ts +79 -0
  11. package/packages/ndpr-toolkit/dist/components/consent/ConsentManager.d.ts +73 -0
  12. package/packages/ndpr-toolkit/dist/components/consent/ConsentStorage.d.ts +41 -0
  13. package/packages/ndpr-toolkit/dist/components/dpia/DPIAQuestionnaire.d.ts +70 -0
  14. package/packages/ndpr-toolkit/dist/components/dpia/DPIAReport.d.ts +40 -0
  15. package/packages/ndpr-toolkit/dist/components/dpia/StepIndicator.d.ts +64 -0
  16. package/packages/ndpr-toolkit/dist/components/dsr/DSRDashboard.d.ts +58 -0
  17. package/packages/ndpr-toolkit/dist/components/dsr/DSRRequestForm.d.ts +74 -0
  18. package/packages/ndpr-toolkit/dist/components/dsr/DSRTracker.d.ts +56 -0
  19. package/packages/ndpr-toolkit/dist/components/policy/PolicyExporter.d.ts +65 -0
  20. package/packages/ndpr-toolkit/dist/components/policy/PolicyGenerator.d.ts +54 -0
  21. package/packages/ndpr-toolkit/dist/components/policy/PolicyPreview.d.ts +71 -0
  22. package/packages/ndpr-toolkit/dist/hooks/useBreach.d.ts +97 -0
  23. package/packages/ndpr-toolkit/dist/hooks/useConsent.d.ts +63 -0
  24. package/packages/ndpr-toolkit/dist/hooks/useDPIA.d.ts +92 -0
  25. package/packages/ndpr-toolkit/dist/hooks/useDSR.d.ts +72 -0
  26. package/packages/ndpr-toolkit/dist/hooks/usePrivacyPolicy.d.ts +87 -0
  27. package/packages/ndpr-toolkit/dist/index.d.ts +31 -0
  28. package/packages/ndpr-toolkit/dist/index.esm.js +2 -0
  29. package/packages/ndpr-toolkit/dist/index.esm.js.map +1 -0
  30. package/packages/ndpr-toolkit/dist/index.js +2 -0
  31. package/packages/ndpr-toolkit/dist/index.js.map +1 -0
  32. package/packages/ndpr-toolkit/dist/setupTests.d.ts +2 -0
  33. package/packages/ndpr-toolkit/dist/types/breach.d.ts +239 -0
  34. package/packages/ndpr-toolkit/dist/types/consent.d.ts +95 -0
  35. package/packages/ndpr-toolkit/dist/types/dpia.d.ts +196 -0
  36. package/packages/ndpr-toolkit/dist/types/dsr.d.ts +162 -0
  37. package/packages/ndpr-toolkit/dist/types/privacy.d.ts +204 -0
  38. package/packages/ndpr-toolkit/dist/utils/breach.d.ts +14 -0
  39. package/packages/ndpr-toolkit/dist/utils/consent.d.ts +10 -0
  40. package/packages/ndpr-toolkit/dist/utils/dpia.d.ts +12 -0
  41. package/packages/ndpr-toolkit/dist/utils/dsr.d.ts +11 -0
  42. package/packages/ndpr-toolkit/dist/utils/privacy.d.ts +12 -0
  43. package/packages/ndpr-toolkit/package-lock.json +8197 -0
  44. package/packages/ndpr-toolkit/package.json +71 -0
  45. package/packages/ndpr-toolkit/rollup.config.js +34 -0
  46. package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
  47. package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
  48. package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
  49. package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
  50. package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
  51. package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
  52. package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
  53. package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
  54. package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
  55. package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
  56. package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
  57. package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
  58. package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
  59. package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
  60. package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
  61. package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
  62. package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
  63. package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
  64. package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
  65. package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
  66. package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
  67. package/packages/ndpr-toolkit/src/index.ts +44 -0
  68. package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
  69. package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
  70. package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
  71. package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
  72. package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
  73. package/packages/ndpr-toolkit/src/types/index.ts +42 -0
  74. package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
  75. package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
  76. package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
  77. package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
  78. package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
  79. package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
  80. package/packages/ndpr-toolkit/tsconfig.json +23 -0
  81. package/postcss.config.mjs +5 -0
  82. package/src/app/accessibility.css +70 -0
  83. package/src/app/favicon.ico +0 -0
  84. package/src/app/globals.css +123 -0
  85. package/src/app/layout.tsx +37 -0
  86. package/src/app/ndpr-demos/breach/page.tsx +354 -0
  87. package/src/app/ndpr-demos/consent/page.tsx +366 -0
  88. package/src/app/ndpr-demos/dpia/page.tsx +495 -0
  89. package/src/app/ndpr-demos/dsr/page.tsx +280 -0
  90. package/src/app/ndpr-demos/page.tsx +73 -0
  91. package/src/app/ndpr-demos/policy/page.tsx +771 -0
  92. package/src/app/page.tsx +452 -0
  93. package/src/components/ErrorBoundary.tsx +90 -0
  94. package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
  95. package/src/components/consent/ConsentBanner.tsx +193 -0
  96. package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +530 -0
  97. package/src/components/dpia/DPIAQuestionnaire.tsx +523 -0
  98. package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
  99. package/src/components/privacy-policy/data.ts +98 -0
  100. package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
  101. package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
  102. package/src/components/privacy-policy/shared/FormField.tsx +79 -0
  103. package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
  104. package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +361 -0
  105. package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
  106. package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
  107. package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
  108. package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +226 -0
  109. package/src/components/ui/Badge.tsx +46 -0
  110. package/src/components/ui/Button.tsx +59 -0
  111. package/src/components/ui/Card.tsx +92 -0
  112. package/src/components/ui/Checkbox.tsx +57 -0
  113. package/src/components/ui/FormField.tsx +50 -0
  114. package/src/components/ui/Input.tsx +38 -0
  115. package/src/components/ui/Loading.tsx +201 -0
  116. package/src/components/ui/Select.tsx +42 -0
  117. package/src/components/ui/TextArea.tsx +38 -0
  118. package/src/components/ui/label.tsx +24 -0
  119. package/src/components/ui/switch.tsx +31 -0
  120. package/src/components/ui/tabs.tsx +66 -0
  121. package/src/hooks/useConsent.ts +70 -0
  122. package/src/hooks/useLoadingState.ts +85 -0
  123. package/src/lib/consentService.ts +144 -0
  124. package/src/lib/dpiaQuestions.ts +188 -0
  125. package/src/lib/requestService.ts +79 -0
  126. package/src/lib/sanitize.ts +108 -0
  127. package/src/lib/storage.ts +222 -0
  128. package/src/lib/utils.ts +6 -0
  129. package/src/types/html-to-docx.d.ts +30 -0
  130. package/src/types/index.ts +77 -0
  131. package/tailwind.config.ts +65 -0
  132. package/tsconfig.json +41 -0
@@ -0,0 +1,373 @@
1
+ import React from 'react';
2
+ import { DPIAResult, DPIASection, DPIARisk } from '../../types/dpia';
3
+
4
+ export interface DPIAReportProps {
5
+ /**
6
+ * The DPIA result to display
7
+ */
8
+ result: DPIAResult;
9
+
10
+ /**
11
+ * The sections of the DPIA questionnaire
12
+ */
13
+ sections: DPIASection[];
14
+
15
+ /**
16
+ * Whether to show the full report or just a summary
17
+ * @default true
18
+ */
19
+ showFullReport?: boolean;
20
+
21
+ /**
22
+ * Whether to allow printing the report
23
+ * @default true
24
+ */
25
+ allowPrint?: boolean;
26
+
27
+ /**
28
+ * Whether to allow exporting the report as PDF
29
+ * @default true
30
+ */
31
+ allowExport?: boolean;
32
+
33
+ /**
34
+ * Callback function called when the report is exported
35
+ */
36
+ onExport?: (format: 'pdf' | 'docx' | 'html') => void;
37
+
38
+ /**
39
+ * Custom CSS class for the report container
40
+ */
41
+ className?: string;
42
+
43
+ /**
44
+ * Custom CSS class for the buttons
45
+ */
46
+ buttonClassName?: string;
47
+ }
48
+
49
+ export const DPIAReport: React.FC<DPIAReportProps> = ({
50
+ result,
51
+ sections,
52
+ showFullReport = true,
53
+ allowPrint = true,
54
+ allowExport = true,
55
+ onExport,
56
+ className = '',
57
+ buttonClassName = ''
58
+ }) => {
59
+ // Format a date from timestamp
60
+ const formatDate = (timestamp: number): string => {
61
+ return new Date(timestamp).toLocaleDateString('en-GB', {
62
+ day: 'numeric',
63
+ month: 'long',
64
+ year: 'numeric'
65
+ });
66
+ };
67
+
68
+ // Get the section title by ID
69
+ const getSectionTitle = (sectionId: string): string => {
70
+ const section = sections.find(s => s.id === sectionId);
71
+ return section?.title || 'Unknown Section';
72
+ };
73
+
74
+ // Get the question text by ID
75
+ const getQuestionText = (questionId: string): string => {
76
+ let questionText = 'Unknown Question';
77
+
78
+ sections.forEach(section => {
79
+ const question = section.questions.find(q => q.id === questionId);
80
+ if (question) {
81
+ questionText = question.text;
82
+ }
83
+ });
84
+
85
+ return questionText;
86
+ };
87
+
88
+ // Get the answer text for a question
89
+ const getAnswerText = (questionId: string): string => {
90
+ const answer = result.answers[questionId];
91
+
92
+ if (answer === undefined || answer === null) {
93
+ return 'Not answered';
94
+ }
95
+
96
+ if (typeof answer === 'boolean') {
97
+ return answer ? 'Yes' : 'No';
98
+ }
99
+
100
+ if (Array.isArray(answer)) {
101
+ return answer.join(', ');
102
+ }
103
+
104
+ return String(answer);
105
+ };
106
+
107
+ // Handle print button click
108
+ const handlePrint = () => {
109
+ window.print();
110
+ };
111
+
112
+ // Handle export button click
113
+ const handleExport = (format: 'pdf' | 'docx' | 'html') => {
114
+ if (onExport) {
115
+ onExport(format);
116
+ }
117
+ };
118
+
119
+ // Render risk level badge
120
+ const renderRiskLevelBadge = (level: 'low' | 'medium' | 'high' | 'critical') => {
121
+ const colorClasses = {
122
+ low: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
123
+ medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
124
+ high: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
125
+ critical: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
126
+ };
127
+
128
+ return (
129
+ <span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[level]}`}>
130
+ {level.charAt(0).toUpperCase() + level.slice(1)}
131
+ </span>
132
+ );
133
+ };
134
+
135
+ return (
136
+ <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 print:shadow-none print:p-0 ${className}`}>
137
+ {/* Report Header */}
138
+ <div className="mb-8 border-b border-gray-200 dark:border-gray-700 pb-6 print:pb-4">
139
+ <div className="flex justify-between items-start">
140
+ <div>
141
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
142
+ Data Protection Impact Assessment Report
143
+ </h1>
144
+ <h2 className="text-xl text-gray-700 dark:text-gray-300 mb-4">
145
+ {result.title}
146
+ </h2>
147
+ </div>
148
+
149
+ {(allowPrint || allowExport) && (
150
+ <div className="flex space-x-2 print:hidden">
151
+ {allowPrint && (
152
+ <button
153
+ onClick={handlePrint}
154
+ className={`px-3 py-1 bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-white rounded hover:bg-gray-300 dark:hover:bg-gray-600 ${buttonClassName}`}
155
+ >
156
+ <span className="flex items-center">
157
+ <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
158
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
159
+ </svg>
160
+ Print
161
+ </span>
162
+ </button>
163
+ )}
164
+
165
+ {allowExport && (
166
+ <div className="relative inline-block">
167
+ <button
168
+ onClick={() => handleExport('pdf')}
169
+ className={`px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 ${buttonClassName}`}
170
+ >
171
+ <span className="flex items-center">
172
+ <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
173
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
174
+ </svg>
175
+ Export PDF
176
+ </span>
177
+ </button>
178
+ </div>
179
+ )}
180
+ </div>
181
+ )}
182
+ </div>
183
+
184
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
185
+ <div>
186
+ <p className="text-sm text-gray-500 dark:text-gray-400">
187
+ <span className="font-medium">Assessor:</span> {result.assessor.name}, {result.assessor.role}
188
+ </p>
189
+ <p className="text-sm text-gray-500 dark:text-gray-400">
190
+ <span className="font-medium">Contact:</span> {result.assessor.email}
191
+ </p>
192
+ </div>
193
+ <div>
194
+ <p className="text-sm text-gray-500 dark:text-gray-400">
195
+ <span className="font-medium">Started:</span> {formatDate(result.startedAt)}
196
+ </p>
197
+ <p className="text-sm text-gray-500 dark:text-gray-400">
198
+ <span className="font-medium">Completed:</span> {result.completedAt ? formatDate(result.completedAt) : 'In progress'}
199
+ </p>
200
+ <p className="text-sm text-gray-500 dark:text-gray-400">
201
+ <span className="font-medium">Next review:</span> {result.reviewDate ? formatDate(result.reviewDate) : 'Not scheduled'}
202
+ </p>
203
+ </div>
204
+ </div>
205
+ </div>
206
+
207
+ {/* Executive Summary */}
208
+ <div className="mb-8">
209
+ <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
210
+ Executive Summary
211
+ </h2>
212
+
213
+ <div className="bg-gray-50 dark:bg-gray-700 p-4 rounded-md mb-4">
214
+ <div className="flex items-center mb-2">
215
+ <span className="font-medium mr-2">Overall Risk Level:</span>
216
+ {renderRiskLevelBadge(result.overallRiskLevel)}
217
+ </div>
218
+
219
+ <div className="mb-2">
220
+ <span className="font-medium">Conclusion:</span> {result.conclusion}
221
+ </div>
222
+
223
+ <div>
224
+ <span className="font-medium">Can Proceed:</span> {result.canProceed ? 'Yes' : 'No'}
225
+ </div>
226
+ </div>
227
+
228
+ <div>
229
+ <h3 className="font-medium text-gray-900 dark:text-white mb-2">
230
+ Processing Activity Description
231
+ </h3>
232
+ <p className="text-gray-700 dark:text-gray-300 mb-4">
233
+ {result.processingDescription}
234
+ </p>
235
+
236
+ {result.recommendations && result.recommendations.length > 0 && (
237
+ <div>
238
+ <h3 className="font-medium text-gray-900 dark:text-white mb-2">
239
+ Key Recommendations
240
+ </h3>
241
+ <ul className="list-disc pl-5 text-gray-700 dark:text-gray-300">
242
+ {result.recommendations.map((recommendation, index) => (
243
+ <li key={index}>{recommendation}</li>
244
+ ))}
245
+ </ul>
246
+ </div>
247
+ )}
248
+ </div>
249
+ </div>
250
+
251
+ {/* Identified Risks */}
252
+ <div className="mb-8">
253
+ <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
254
+ Identified Risks
255
+ </h2>
256
+
257
+ {result.risks.length === 0 ? (
258
+ <p className="text-gray-700 dark:text-gray-300">No risks identified.</p>
259
+ ) : (
260
+ <div className="overflow-x-auto">
261
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
262
+ <thead className="bg-gray-50 dark:bg-gray-700">
263
+ <tr>
264
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
265
+ Risk
266
+ </th>
267
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
268
+ Likelihood
269
+ </th>
270
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
271
+ Impact
272
+ </th>
273
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
274
+ Risk Level
275
+ </th>
276
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
277
+ Mitigated
278
+ </th>
279
+ </tr>
280
+ </thead>
281
+ <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
282
+ {result.risks.map((risk) => (
283
+ <tr key={risk.id}>
284
+ <td className="px-6 py-4 whitespace-normal text-sm text-gray-900 dark:text-gray-100">
285
+ {risk.description}
286
+ </td>
287
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
288
+ {risk.likelihood} / 5
289
+ </td>
290
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
291
+ {risk.impact} / 5
292
+ </td>
293
+ <td className="px-6 py-4 whitespace-nowrap text-sm">
294
+ {renderRiskLevelBadge(risk.level)}
295
+ </td>
296
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
297
+ {risk.mitigated ? (
298
+ <span className="text-green-600 dark:text-green-400">Yes</span>
299
+ ) : (
300
+ <span className="text-red-600 dark:text-red-400">No</span>
301
+ )}
302
+ </td>
303
+ </tr>
304
+ ))}
305
+ </tbody>
306
+ </table>
307
+ </div>
308
+ )}
309
+ </div>
310
+
311
+ {/* Full Assessment Details */}
312
+ {showFullReport && (
313
+ <div>
314
+ <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
315
+ Full Assessment Details
316
+ </h2>
317
+
318
+ {sections.map((section) => {
319
+ const sectionQuestions = section.questions.filter(question =>
320
+ result.answers[question.id] !== undefined
321
+ );
322
+
323
+ if (sectionQuestions.length === 0) {
324
+ return null;
325
+ }
326
+
327
+ return (
328
+ <div key={section.id} className="mb-6">
329
+ <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
330
+ {section.title}
331
+ </h3>
332
+
333
+ <div className="overflow-x-auto">
334
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
335
+ <thead className="bg-gray-50 dark:bg-gray-700">
336
+ <tr>
337
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
338
+ Question
339
+ </th>
340
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
341
+ Answer
342
+ </th>
343
+ </tr>
344
+ </thead>
345
+ <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
346
+ {sectionQuestions.map((question) => (
347
+ <tr key={question.id}>
348
+ <td className="px-6 py-4 whitespace-normal text-sm text-gray-900 dark:text-gray-100">
349
+ {question.text}
350
+ </td>
351
+ <td className="px-6 py-4 whitespace-normal text-sm text-gray-500 dark:text-gray-400">
352
+ {getAnswerText(question.id)}
353
+ </td>
354
+ </tr>
355
+ ))}
356
+ </tbody>
357
+ </table>
358
+ </div>
359
+ </div>
360
+ );
361
+ })}
362
+ </div>
363
+ )}
364
+
365
+ {/* Footer */}
366
+ <div className="mt-8 pt-4 border-t border-gray-200 dark:border-gray-700 text-sm text-gray-500 dark:text-gray-400">
367
+ <p>This DPIA was conducted in accordance with the Nigeria Data Protection Regulation (NDPR).</p>
368
+ <p>DPIA Report Version: {result.version}</p>
369
+ <p>Generated on: {new Date().toLocaleDateString()}</p>
370
+ </div>
371
+ </div>
372
+ );
373
+ };
@@ -0,0 +1,174 @@
1
+ import React from 'react';
2
+
3
+ export interface Step {
4
+ /**
5
+ * Unique identifier for the step
6
+ */
7
+ id: string;
8
+
9
+ /**
10
+ * Display label for the step
11
+ */
12
+ label: string;
13
+
14
+ /**
15
+ * Optional description for the step
16
+ */
17
+ description?: string;
18
+
19
+ /**
20
+ * Whether the step is completed
21
+ */
22
+ completed: boolean;
23
+
24
+ /**
25
+ * Whether the step is the current active step
26
+ */
27
+ active: boolean;
28
+
29
+ /**
30
+ * Optional icon for the step
31
+ */
32
+ icon?: React.ReactNode;
33
+ }
34
+
35
+ export interface StepIndicatorProps {
36
+ /**
37
+ * Array of steps to display
38
+ */
39
+ steps: Step[];
40
+
41
+ /**
42
+ * Callback function called when a step is clicked
43
+ */
44
+ onStepClick?: (stepId: string) => void;
45
+
46
+ /**
47
+ * Whether the steps are clickable
48
+ * @default true
49
+ */
50
+ clickable?: boolean;
51
+
52
+ /**
53
+ * Orientation of the step indicator
54
+ * @default "horizontal"
55
+ */
56
+ orientation?: 'horizontal' | 'vertical';
57
+
58
+ /**
59
+ * Custom CSS class for the container
60
+ */
61
+ className?: string;
62
+
63
+ /**
64
+ * Custom CSS class for the active step
65
+ */
66
+ activeStepClassName?: string;
67
+
68
+ /**
69
+ * Custom CSS class for completed steps
70
+ */
71
+ completedStepClassName?: string;
72
+
73
+ /**
74
+ * Custom CSS class for incomplete steps
75
+ */
76
+ incompleteStepClassName?: string;
77
+ }
78
+
79
+ export const StepIndicator: React.FC<StepIndicatorProps> = ({
80
+ steps,
81
+ onStepClick,
82
+ clickable = true,
83
+ orientation = 'horizontal',
84
+ className = '',
85
+ activeStepClassName = '',
86
+ completedStepClassName = '',
87
+ incompleteStepClassName = ''
88
+ }) => {
89
+ const handleStepClick = (stepId: string) => {
90
+ if (clickable && onStepClick) {
91
+ onStepClick(stepId);
92
+ }
93
+ };
94
+
95
+ const isVertical = orientation === 'vertical';
96
+
97
+ return (
98
+ <div
99
+ className={`${className} ${
100
+ isVertical
101
+ ? 'flex flex-col space-y-4'
102
+ : 'flex items-center justify-between'
103
+ }`}
104
+ >
105
+ {steps.map((step, index) => {
106
+ const isLast = index === steps.length - 1;
107
+ const stepClassName = step.active
108
+ ? `font-medium ${activeStepClassName || 'text-blue-600 dark:text-blue-400'}`
109
+ : step.completed
110
+ ? `${completedStepClassName || 'text-green-600 dark:text-green-400'}`
111
+ : `${incompleteStepClassName || 'text-gray-500 dark:text-gray-400'}`;
112
+
113
+ return (
114
+ <React.Fragment key={step.id}>
115
+ <div
116
+ className={`
117
+ ${isVertical ? 'flex items-start' : 'flex flex-col items-center'}
118
+ ${clickable ? 'cursor-pointer' : ''}
119
+ `}
120
+ onClick={() => handleStepClick(step.id)}
121
+ >
122
+ <div className={`
123
+ flex items-center justify-center
124
+ ${isVertical ? 'mr-4' : ''}
125
+ `}>
126
+ <div className={`
127
+ flex items-center justify-center
128
+ w-8 h-8 rounded-full
129
+ ${step.active
130
+ ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 border-2 border-blue-600 dark:border-blue-400'
131
+ : step.completed
132
+ ? 'bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400 border-2 border-green-600 dark:border-green-400'
133
+ : 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 border-2 border-gray-300 dark:border-gray-600'
134
+ }
135
+ `}>
136
+ {step.icon ? (
137
+ step.icon
138
+ ) : step.completed ? (
139
+ <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
140
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
141
+ </svg>
142
+ ) : (
143
+ <span>{index + 1}</span>
144
+ )}
145
+ </div>
146
+ </div>
147
+
148
+ <div className={`
149
+ ${isVertical ? 'flex-1' : 'mt-2 text-center'}
150
+ `}>
151
+ <div className={`text-sm font-medium ${stepClassName}`}>
152
+ {step.label}
153
+ </div>
154
+ {step.description && (
155
+ <div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
156
+ {step.description}
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+
162
+ {!isLast && (
163
+ <div className={`
164
+ ${isVertical
165
+ ? 'ml-4 h-8 border-l-2 border-gray-300 dark:border-gray-600'
166
+ : 'w-full border-t-2 border-gray-300 dark:border-gray-600 hidden sm:block'}
167
+ `} />
168
+ )}
169
+ </React.Fragment>
170
+ );
171
+ })}
172
+ </div>
173
+ );
174
+ };