@tantainnovative/ndpr-toolkit 1.0.1 → 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 -431
  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} +14 -1
  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,541 @@
1
+ import React, { useState } from 'react';
2
+
3
+ export interface PolicyExporterProps {
4
+ /**
5
+ * The policy content to export
6
+ */
7
+ content: string;
8
+
9
+ /**
10
+ * The policy title
11
+ */
12
+ title?: string;
13
+
14
+ /**
15
+ * The organization name to include in the exported policy
16
+ */
17
+ organizationName?: string;
18
+
19
+ /**
20
+ * The last updated date to include in the exported policy
21
+ */
22
+ lastUpdated?: Date;
23
+
24
+ /**
25
+ * Callback function called when the export is complete
26
+ */
27
+ onExportComplete?: (format: string, url: string) => void;
28
+
29
+ /**
30
+ * Title displayed on the exporter
31
+ * @default "Export Privacy Policy"
32
+ */
33
+ componentTitle?: string;
34
+
35
+ /**
36
+ * Description text displayed on the exporter
37
+ * @default "Export your NDPR-compliant privacy policy in various formats."
38
+ */
39
+ description?: string;
40
+
41
+ /**
42
+ * Custom CSS class for the exporter
43
+ */
44
+ className?: string;
45
+
46
+ /**
47
+ * Custom CSS class for the buttons
48
+ */
49
+ buttonClassName?: string;
50
+
51
+ /**
52
+ * Whether to show the export history
53
+ * @default true
54
+ */
55
+ showExportHistory?: boolean;
56
+
57
+ /**
58
+ * Whether to include the NDPR compliance notice in the exported policy
59
+ * @default true
60
+ */
61
+ includeComplianceNotice?: boolean;
62
+
63
+ /**
64
+ * Whether to include the organization logo in the exported policy
65
+ * @default false
66
+ */
67
+ includeLogo?: boolean;
68
+
69
+ /**
70
+ * URL of the organization logo
71
+ */
72
+ logoUrl?: string;
73
+
74
+ /**
75
+ * Custom CSS styles for the exported policy
76
+ */
77
+ customStyles?: string;
78
+ }
79
+
80
+ interface ExportRecord {
81
+ id: string;
82
+ format: string;
83
+ timestamp: number;
84
+ url: string;
85
+ filename: string;
86
+ }
87
+
88
+ export const PolicyExporter: React.FC<PolicyExporterProps> = ({
89
+ content,
90
+ title = "Privacy Policy",
91
+ organizationName,
92
+ lastUpdated = new Date(),
93
+ onExportComplete,
94
+ componentTitle = "Export Privacy Policy",
95
+ description = "Export your NDPR-compliant privacy policy in various formats.",
96
+ className = "",
97
+ buttonClassName = "",
98
+ showExportHistory = true,
99
+ includeComplianceNotice = true,
100
+ includeLogo = false,
101
+ logoUrl,
102
+ customStyles
103
+ }) => {
104
+ const [exportHistory, setExportHistory] = useState<ExportRecord[]>([]);
105
+ const [selectedFormat, setSelectedFormat] = useState<string>('pdf');
106
+ const [isExporting, setIsExporting] = useState<boolean>(false);
107
+ const [exportError, setExportError] = useState<string | null>(null);
108
+ const [customFilename, setCustomFilename] = useState<string>('');
109
+ const [customHeader, setCustomHeader] = useState<string>('');
110
+ const [customFooter, setCustomFooter] = useState<string>('');
111
+ const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(false);
112
+
113
+ // Generate a default filename based on the organization name and format
114
+ const generateDefaultFilename = (format: string): string => {
115
+ const dateStr = new Date().toISOString().split('T')[0];
116
+ const orgStr = organizationName ?
117
+ organizationName.toLowerCase().replace(/[^a-z0-9]+/g, '-') :
118
+ 'privacy-policy';
119
+
120
+ return `${orgStr}-privacy-policy-${dateStr}.${format.toLowerCase()}`;
121
+ };
122
+
123
+ // Get the actual filename to use
124
+ const getFilename = (format: string): string => {
125
+ if (customFilename) {
126
+ // Ensure the filename has the correct extension
127
+ if (customFilename.endsWith(`.${format.toLowerCase()}`)) {
128
+ return customFilename;
129
+ } else {
130
+ return `${customFilename}.${format.toLowerCase()}`;
131
+ }
132
+ }
133
+
134
+ return generateDefaultFilename(format);
135
+ };
136
+
137
+ // Generate HTML content for export
138
+ const generateHTMLContent = (): string => {
139
+ const fullTitle = organizationName ? `${organizationName} ${title}` : title;
140
+ const dateStr = lastUpdated.toLocaleDateString();
141
+
142
+ let html = `<!DOCTYPE html>
143
+ <html lang="en">
144
+ <head>
145
+ <meta charset="UTF-8">
146
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
147
+ <title>${fullTitle}</title>
148
+ <style>
149
+ body {
150
+ font-family: Arial, sans-serif;
151
+ line-height: 1.6;
152
+ color: #333;
153
+ max-width: 800px;
154
+ margin: 0 auto;
155
+ padding: 20px;
156
+ }
157
+ h1 {
158
+ font-size: 24px;
159
+ margin-bottom: 10px;
160
+ }
161
+ h2 {
162
+ font-size: 20px;
163
+ margin-top: 30px;
164
+ margin-bottom: 10px;
165
+ border-bottom: 1px solid #eee;
166
+ padding-bottom: 5px;
167
+ }
168
+ h3 {
169
+ font-size: 18px;
170
+ margin-top: 20px;
171
+ margin-bottom: 10px;
172
+ }
173
+ p {
174
+ margin-bottom: 15px;
175
+ }
176
+ .header {
177
+ text-align: center;
178
+ margin-bottom: 30px;
179
+ }
180
+ .footer {
181
+ margin-top: 50px;
182
+ text-align: center;
183
+ font-size: 12px;
184
+ color: #666;
185
+ border-top: 1px solid #eee;
186
+ padding-top: 20px;
187
+ }
188
+ .logo {
189
+ max-width: 200px;
190
+ margin-bottom: 20px;
191
+ }
192
+ .last-updated {
193
+ font-size: 12px;
194
+ color: #666;
195
+ margin-bottom: 30px;
196
+ }
197
+ .compliance-notice {
198
+ background-color: #f8f9fa;
199
+ border: 1px solid #e9ecef;
200
+ padding: 15px;
201
+ margin-bottom: 30px;
202
+ font-size: 14px;
203
+ }
204
+ ${customStyles || ''}
205
+ </style>
206
+ </head>
207
+ <body>
208
+ <div class="header">
209
+ ${includeLogo && logoUrl ? `<img src="${logoUrl}" alt="${organizationName || 'Company'} Logo" class="logo">` : ''}
210
+ ${customHeader ? `<div class="custom-header">${customHeader}</div>` : ''}
211
+ <h1>${fullTitle}</h1>
212
+ <div class="last-updated">Last Updated: ${dateStr}</div>
213
+ </div>`;
214
+
215
+ if (includeComplianceNotice) {
216
+ html += `
217
+ <div class="compliance-notice">
218
+ <strong>NDPR Compliance Notice:</strong> This privacy policy has been created to comply with the Nigeria Data Protection Regulation (NDPR).
219
+ It outlines how we collect, use, disclose, and protect your personal information in accordance with NDPR requirements.
220
+ </div>`;
221
+ }
222
+
223
+ // Convert markdown content to HTML
224
+ const htmlContent = content
225
+ .replace(/^## (.*?)$/gm, '<h2>$1</h2>')
226
+ .replace(/^### (.*?)$/gm, '<h3>$1</h3>')
227
+ .replace(/\n\n/g, '</p><p>')
228
+ .replace(/\n/g, '<br>');
229
+
230
+ html += `
231
+ <div class="content">
232
+ <p>${htmlContent}</p>
233
+ </div>
234
+
235
+ <div class="footer">
236
+ ${customFooter ? `<div class="custom-footer">${customFooter}</div>` : ''}
237
+ <p>&copy; ${new Date().getFullYear()} ${organizationName || 'Company'}. All rights reserved.</p>
238
+ </div>
239
+ </body>
240
+ </html>`;
241
+
242
+ return html;
243
+ };
244
+
245
+ // Handle export button click
246
+ const handleExport = async () => {
247
+ setIsExporting(true);
248
+ setExportError(null);
249
+
250
+ try {
251
+ const format = selectedFormat.toLowerCase();
252
+ let url = '';
253
+ let blob: Blob;
254
+
255
+ switch (format) {
256
+ case 'pdf':
257
+ // In a real implementation, this would use a PDF generation library
258
+ // For this example, we'll just create an HTML file with a note
259
+ const htmlForPdf = generateHTMLContent();
260
+ blob = new Blob([htmlForPdf], { type: 'text/html' });
261
+ url = URL.createObjectURL(blob);
262
+ break;
263
+
264
+ case 'docx':
265
+ // In a real implementation, this would use a DOCX generation library
266
+ // For this example, we'll just create a text file with a note
267
+ blob = new Blob([content], { type: 'text/plain' });
268
+ url = URL.createObjectURL(blob);
269
+ break;
270
+
271
+ case 'html':
272
+ const html = generateHTMLContent();
273
+ blob = new Blob([html], { type: 'text/html' });
274
+ url = URL.createObjectURL(blob);
275
+ break;
276
+
277
+ case 'markdown':
278
+ default:
279
+ blob = new Blob([content], { type: 'text/markdown' });
280
+ url = URL.createObjectURL(blob);
281
+ break;
282
+ }
283
+
284
+ // Create a download link and trigger it
285
+ const filename = getFilename(format);
286
+ const element = document.createElement('a');
287
+ element.href = url;
288
+ element.download = filename;
289
+ document.body.appendChild(element);
290
+ element.click();
291
+ document.body.removeChild(element);
292
+
293
+ // Add to export history
294
+ const exportRecord: ExportRecord = {
295
+ id: `export_${Date.now()}`,
296
+ format,
297
+ timestamp: Date.now(),
298
+ url,
299
+ filename
300
+ };
301
+
302
+ setExportHistory(prevHistory => [exportRecord, ...prevHistory]);
303
+
304
+ // Call the callback
305
+ if (onExportComplete) {
306
+ onExportComplete(format, url);
307
+ }
308
+ } catch (error) {
309
+ console.error('Export error:', error);
310
+ setExportError('An error occurred during export. Please try again.');
311
+ } finally {
312
+ setIsExporting(false);
313
+ }
314
+ };
315
+
316
+ // Render export format options
317
+ const renderFormatOptions = () => {
318
+ const formats = [
319
+ { value: 'pdf', label: 'PDF Document (.pdf)' },
320
+ { value: 'docx', label: 'Word Document (.docx)' },
321
+ { value: 'html', label: 'Web Page (.html)' },
322
+ { value: 'markdown', label: 'Markdown (.md)' }
323
+ ];
324
+
325
+ return (
326
+ <div className="mb-6">
327
+ <label htmlFor="export-format" className="block text-sm font-medium mb-1">
328
+ Export Format
329
+ </label>
330
+ <select
331
+ id="export-format"
332
+ value={selectedFormat}
333
+ onChange={e => setSelectedFormat(e.target.value)}
334
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
335
+ >
336
+ {formats.map(format => (
337
+ <option key={format.value} value={format.value}>
338
+ {format.label}
339
+ </option>
340
+ ))}
341
+ </select>
342
+ </div>
343
+ );
344
+ };
345
+
346
+ // Render advanced options
347
+ const renderAdvancedOptions = () => {
348
+ if (!showAdvancedOptions) {
349
+ return (
350
+ <button
351
+ type="button"
352
+ onClick={() => setShowAdvancedOptions(true)}
353
+ className="text-blue-600 dark:text-blue-400 text-sm mb-6"
354
+ >
355
+ Show Advanced Options
356
+ </button>
357
+ );
358
+ }
359
+
360
+ return (
361
+ <div className="mb-6 space-y-4 border border-gray-200 dark:border-gray-700 rounded-md p-4">
362
+ <div className="flex justify-between items-center">
363
+ <h3 className="text-md font-medium">Advanced Export Options</h3>
364
+ <button
365
+ type="button"
366
+ onClick={() => setShowAdvancedOptions(false)}
367
+ className="text-blue-600 dark:text-blue-400 text-sm"
368
+ >
369
+ Hide Advanced Options
370
+ </button>
371
+ </div>
372
+
373
+ <div>
374
+ <label htmlFor="custom-filename" className="block text-sm font-medium mb-1">
375
+ Custom Filename
376
+ </label>
377
+ <input
378
+ type="text"
379
+ id="custom-filename"
380
+ value={customFilename}
381
+ onChange={e => setCustomFilename(e.target.value)}
382
+ placeholder={generateDefaultFilename(selectedFormat)}
383
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
384
+ />
385
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
386
+ Leave blank to use the default filename format.
387
+ </p>
388
+ </div>
389
+
390
+ <div>
391
+ <label htmlFor="custom-header" className="block text-sm font-medium mb-1">
392
+ Custom Header HTML (for HTML/PDF exports)
393
+ </label>
394
+ <textarea
395
+ id="custom-header"
396
+ value={customHeader}
397
+ onChange={e => setCustomHeader(e.target.value)}
398
+ rows={3}
399
+ placeholder="<div>Custom header content</div>"
400
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
401
+ />
402
+ </div>
403
+
404
+ <div>
405
+ <label htmlFor="custom-footer" className="block text-sm font-medium mb-1">
406
+ Custom Footer HTML (for HTML/PDF exports)
407
+ </label>
408
+ <textarea
409
+ id="custom-footer"
410
+ value={customFooter}
411
+ onChange={e => setCustomFooter(e.target.value)}
412
+ rows={3}
413
+ placeholder="<div>Custom footer content</div>"
414
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
415
+ />
416
+ </div>
417
+
418
+ <div className="flex items-start">
419
+ <div className="flex items-center h-5">
420
+ <input
421
+ id="include-compliance-notice"
422
+ type="checkbox"
423
+ checked={includeComplianceNotice}
424
+ onChange={e => setShowAdvancedOptions(e.target.checked)}
425
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
426
+ />
427
+ </div>
428
+ <div className="ml-3 text-sm">
429
+ <label htmlFor="include-compliance-notice" className="font-medium text-gray-900 dark:text-white">
430
+ Include NDPR Compliance Notice
431
+ </label>
432
+ <p className="text-gray-500 dark:text-gray-400">
433
+ Adds a notice explaining that this policy complies with NDPR requirements.
434
+ </p>
435
+ </div>
436
+ </div>
437
+ </div>
438
+ );
439
+ };
440
+
441
+ // Render export history
442
+ const renderExportHistory = () => {
443
+ if (!showExportHistory || exportHistory.length === 0) {
444
+ return null;
445
+ }
446
+
447
+ return (
448
+ <div className="mt-6">
449
+ <h3 className="text-lg font-medium mb-3">Export History</h3>
450
+ <div className="bg-gray-50 dark:bg-gray-700 rounded-md overflow-hidden">
451
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
452
+ <thead className="bg-gray-100 dark:bg-gray-800">
453
+ <tr>
454
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
455
+ Date
456
+ </th>
457
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
458
+ Format
459
+ </th>
460
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
461
+ Filename
462
+ </th>
463
+ <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
464
+ Actions
465
+ </th>
466
+ </tr>
467
+ </thead>
468
+ <tbody className="bg-white dark:bg-gray-700 divide-y divide-gray-200 dark:divide-gray-600">
469
+ {exportHistory.map(record => (
470
+ <tr key={record.id}>
471
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
472
+ {new Date(record.timestamp).toLocaleString()}
473
+ </td>
474
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
475
+ {record.format.toUpperCase()}
476
+ </td>
477
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
478
+ {record.filename}
479
+ </td>
480
+ <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
481
+ <a
482
+ href={record.url}
483
+ download={record.filename}
484
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 mr-4"
485
+ >
486
+ Download
487
+ </a>
488
+ </td>
489
+ </tr>
490
+ ))}
491
+ </tbody>
492
+ </table>
493
+ </div>
494
+ </div>
495
+ );
496
+ };
497
+
498
+ return (
499
+ <div className={`bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md ${className}`}>
500
+ <h2 className="text-xl font-bold mb-2">{componentTitle}</h2>
501
+ <p className="mb-6 text-gray-600 dark:text-gray-300">{description}</p>
502
+
503
+ {/* Format Selection */}
504
+ {renderFormatOptions()}
505
+
506
+ {/* Advanced Options */}
507
+ {renderAdvancedOptions()}
508
+
509
+ {/* Export Button */}
510
+ <div className="mb-6">
511
+ <button
512
+ onClick={handleExport}
513
+ disabled={isExporting}
514
+ className={`px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${buttonClassName} ${isExporting ? 'opacity-70 cursor-not-allowed' : ''}`}
515
+ >
516
+ {isExporting ? 'Exporting...' : `Export as ${selectedFormat.toUpperCase()}`}
517
+ </button>
518
+
519
+ {exportError && (
520
+ <p className="mt-2 text-sm text-red-600 dark:text-red-500">
521
+ {exportError}
522
+ </p>
523
+ )}
524
+ </div>
525
+
526
+ {/* Export Tips */}
527
+ <div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-md">
528
+ <h3 className="text-sm font-bold text-blue-800 dark:text-blue-200 mb-2">Export Tips</h3>
529
+ <ul className="text-blue-700 dark:text-blue-300 text-sm list-disc list-inside space-y-1">
530
+ <li>PDF format is recommended for printing or sharing with stakeholders.</li>
531
+ <li>HTML format is ideal for publishing on your website.</li>
532
+ <li>DOCX format allows for further editing in Microsoft Word or similar applications.</li>
533
+ <li>Markdown format is useful for version control systems or technical documentation.</li>
534
+ </ul>
535
+ </div>
536
+
537
+ {/* Export History */}
538
+ {renderExportHistory()}
539
+ </div>
540
+ );
541
+ };