@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.
- package/.claude/settings.local.json +20 -0
- package/.eslintrc.json +10 -0
- package/.github/workflows/ci.yml +36 -0
- package/.github/workflows/nextjs.yml +104 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.lintstagedrc.js +4 -0
- package/.nvmrc +1 -0
- package/.versionrc +17 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +90 -0
- package/CNAME +1 -0
- package/CONTRIBUTING.md +87 -0
- package/README.md +84 -447
- package/RELEASE-NOTES-v1.0.0.md +140 -0
- package/RELEASE-NOTES-v1.0.1.md +69 -0
- package/SECURITY.md +21 -0
- package/commitlint.config.js +36 -0
- package/components.json +21 -0
- package/eslint.config.mjs +16 -0
- package/jest.config.js +31 -0
- package/jest.setup.js +15 -0
- package/next.config.js +15 -0
- package/next.config.ts +62 -0
- package/package.json +70 -52
- package/packages/ndpr-toolkit/README.md +467 -0
- package/packages/ndpr-toolkit/jest.config.js +23 -0
- package/packages/ndpr-toolkit/package-lock.json +8197 -0
- package/packages/ndpr-toolkit/package.json +71 -0
- package/packages/ndpr-toolkit/rollup.config.js +34 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentBanner.test.tsx +119 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentManager.test.tsx +122 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentStorage.test.tsx +270 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRDashboard.test.tsx +199 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRRequestForm.test.tsx +224 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRTracker.test.tsx +104 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useConsent.test.tsx +161 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useDSR.test.tsx +330 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/breach.test.ts +149 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/consent.test.ts +88 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dpia.test.ts +160 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dsr.test.ts +110 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/privacy.test.ts +97 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
- package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
- package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
- package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
- package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
- package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
- package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
- package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
- package/{dist/index.d.ts → packages/ndpr-toolkit/src/index.ts} +13 -0
- package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
- package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
- package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
- package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
- package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
- package/packages/ndpr-toolkit/src/types/index.ts +42 -0
- package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
- package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
- package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
- package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
- package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
- package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
- package/packages/ndpr-toolkit/tsconfig.json +23 -0
- package/postcss.config.mjs +5 -0
- package/public/NDPR TOOLKIT.svg +1 -0
- package/public/favicon/android-chrome-192x192.png +0 -0
- package/public/favicon/android-chrome-512x512.png +0 -0
- package/public/favicon/apple-touch-icon.png +0 -0
- package/public/favicon/favicon-16x16.png +0 -0
- package/public/favicon/favicon-32x32.png +0 -0
- package/public/favicon/site.webmanifest +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/ndpr-toolkit-logo.svg +108 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/__tests__/example.test.ts +13 -0
- package/src/__tests__/requestService.test.ts +57 -0
- package/src/app/accessibility.css +70 -0
- package/src/app/docs/components/DocLayout.tsx +267 -0
- package/src/app/docs/components/breach-notification/page.tsx +797 -0
- package/src/app/docs/components/consent-management/page.tsx +576 -0
- package/src/app/docs/components/data-subject-rights/page.tsx +511 -0
- package/src/app/docs/components/dpia-questionnaire/layout.tsx +15 -0
- package/src/app/docs/components/dpia-questionnaire/metadata.ts +31 -0
- package/src/app/docs/components/dpia-questionnaire/page.tsx +666 -0
- package/src/app/docs/components/hooks/page.tsx +305 -0
- package/src/app/docs/components/page.tsx +84 -0
- package/src/app/docs/components/privacy-policy-generator/page.tsx +634 -0
- package/src/app/docs/guides/breach-notification-process/components/BestPractices.tsx +123 -0
- package/src/app/docs/guides/breach-notification-process/components/ImplementationSteps.tsx +328 -0
- package/src/app/docs/guides/breach-notification-process/components/Introduction.tsx +28 -0
- package/src/app/docs/guides/breach-notification-process/components/NotificationTimeline.tsx +91 -0
- package/src/app/docs/guides/breach-notification-process/components/Resources.tsx +118 -0
- package/src/app/docs/guides/breach-notification-process/page.tsx +39 -0
- package/src/app/docs/guides/conducting-dpia/page.tsx +593 -0
- package/src/app/docs/guides/data-subject-requests/page.tsx +666 -0
- package/src/app/docs/guides/managing-consent/page.tsx +738 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ComplianceChecklist.tsx +296 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ImplementationTools.tsx +145 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Introduction.tsx +33 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/KeyRequirements.tsx +99 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Resources.tsx +159 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/page.tsx +38 -0
- package/src/app/docs/guides/page.tsx +67 -0
- package/src/app/docs/layout.tsx +15 -0
- package/src/app/docs/metadata.ts +31 -0
- package/src/app/docs/page.tsx +572 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/ndpr-demos/breach/page.tsx +354 -0
- package/src/app/ndpr-demos/consent/page.tsx +366 -0
- package/src/app/ndpr-demos/dpia/page.tsx +495 -0
- package/src/app/ndpr-demos/dsr/page.tsx +280 -0
- package/src/app/ndpr-demos/page.tsx +73 -0
- package/src/app/ndpr-demos/policy/page.tsx +771 -0
- package/src/app/page.tsx +452 -0
- package/src/components/ErrorBoundary.tsx +90 -0
- package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
- package/src/components/consent/ConsentBanner.tsx +159 -0
- package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +419 -0
- package/src/components/docs/DocLayout.tsx +289 -0
- package/src/components/docs/index.ts +2 -0
- package/src/components/dpia/DPIAQuestionnaire.tsx +483 -0
- package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
- package/src/components/privacy-policy/data.ts +98 -0
- package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
- package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
- package/src/components/privacy-policy/shared/FormField.tsx +79 -0
- package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
- package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +335 -0
- package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
- package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
- package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
- package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +172 -0
- package/src/components/ui/Badge.tsx +46 -0
- package/src/components/ui/Button.tsx +59 -0
- package/src/components/ui/Card.tsx +92 -0
- package/src/components/ui/Checkbox.tsx +57 -0
- package/src/components/ui/FormField.tsx +50 -0
- package/src/components/ui/Input.tsx +38 -0
- package/src/components/ui/Loading.tsx +201 -0
- package/src/components/ui/Select.tsx +42 -0
- package/src/components/ui/TextArea.tsx +38 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/hooks/useConsent.ts +64 -0
- package/src/hooks/useLoadingState.ts +85 -0
- package/src/lib/consentService.ts +137 -0
- package/src/lib/dpiaQuestions.ts +148 -0
- package/src/lib/requestService.ts +75 -0
- package/src/lib/sanitize.ts +108 -0
- package/src/lib/storage.ts +222 -0
- package/src/lib/utils.ts +6 -0
- package/src/types/html-to-docx.d.ts +30 -0
- package/src/types/index.ts +72 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +41 -0
- package/dist/components/breach/BreachNotificationManager.d.ts +0 -62
- package/dist/components/breach/BreachReportForm.d.ts +0 -66
- package/dist/components/breach/BreachRiskAssessment.d.ts +0 -50
- package/dist/components/breach/RegulatoryReportGenerator.d.ts +0 -94
- package/dist/components/consent/ConsentBanner.d.ts +0 -79
- package/dist/components/consent/ConsentManager.d.ts +0 -73
- package/dist/components/consent/ConsentStorage.d.ts +0 -41
- package/dist/components/dpia/DPIAQuestionnaire.d.ts +0 -70
- package/dist/components/dpia/DPIAReport.d.ts +0 -40
- package/dist/components/dpia/StepIndicator.d.ts +0 -64
- package/dist/components/dsr/DSRDashboard.d.ts +0 -58
- package/dist/components/dsr/DSRRequestForm.d.ts +0 -74
- package/dist/components/dsr/DSRTracker.d.ts +0 -56
- package/dist/components/policy/PolicyExporter.d.ts +0 -65
- package/dist/components/policy/PolicyGenerator.d.ts +0 -54
- package/dist/components/policy/PolicyPreview.d.ts +0 -71
- package/dist/hooks/useBreach.d.ts +0 -97
- package/dist/hooks/useConsent.d.ts +0 -63
- package/dist/hooks/useDPIA.d.ts +0 -92
- package/dist/hooks/useDSR.d.ts +0 -72
- package/dist/hooks/usePrivacyPolicy.d.ts +0 -87
- package/dist/index.esm.js +0 -2
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/setupTests.d.ts +0 -2
- package/dist/types/breach.d.ts +0 -239
- package/dist/types/consent.d.ts +0 -95
- package/dist/types/dpia.d.ts +0 -196
- package/dist/types/dsr.d.ts +0 -162
- package/dist/types/privacy.d.ts +0 -204
- package/dist/utils/breach.d.ts +0 -14
- package/dist/utils/consent.d.ts +0 -10
- package/dist/utils/dpia.d.ts +0 -12
- package/dist/utils/dsr.d.ts +0 -11
- package/dist/utils/privacy.d.ts +0 -12
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { BreachSeverity } from '@/types';
|
|
5
|
+
import { Input } from '@/components/ui/Input';
|
|
6
|
+
import { TextArea } from '@/components/ui/TextArea';
|
|
7
|
+
import { Button } from '@/components/ui/Button';
|
|
8
|
+
import { FormField } from '@/components/ui/FormField';
|
|
9
|
+
import { Checkbox } from '@/components/ui/Checkbox';
|
|
10
|
+
|
|
11
|
+
interface BreachNotificationFormProps {
|
|
12
|
+
onSubmit: (data: {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
discoveryDate: string;
|
|
16
|
+
affectedDataSubjects: number;
|
|
17
|
+
dataCategories: string[];
|
|
18
|
+
severity: BreachSeverity;
|
|
19
|
+
mitigationSteps: string[];
|
|
20
|
+
reportedToAuthorities: boolean;
|
|
21
|
+
reportedToDataSubjects: boolean;
|
|
22
|
+
}) => void;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function BreachNotificationForm({
|
|
27
|
+
onSubmit,
|
|
28
|
+
className = '',
|
|
29
|
+
}: BreachNotificationFormProps) {
|
|
30
|
+
const [formData, setFormData] = useState({
|
|
31
|
+
title: '',
|
|
32
|
+
description: '',
|
|
33
|
+
discoveryDate: '',
|
|
34
|
+
affectedDataSubjects: 0,
|
|
35
|
+
dataCategories: [] as string[],
|
|
36
|
+
severity: 'medium' as BreachSeverity,
|
|
37
|
+
mitigationSteps: [] as string[],
|
|
38
|
+
reportedToAuthorities: false,
|
|
39
|
+
reportedToDataSubjects: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
44
|
+
const [newMitigationStep, setNewMitigationStep] = useState('');
|
|
45
|
+
const [newDataCategory, setNewDataCategory] = useState('');
|
|
46
|
+
|
|
47
|
+
// Default data categories
|
|
48
|
+
const defaultDataCategories = [
|
|
49
|
+
'Personal identifiers (name, ID numbers)',
|
|
50
|
+
'Contact information',
|
|
51
|
+
'Financial information',
|
|
52
|
+
'Health information',
|
|
53
|
+
'Biometric data',
|
|
54
|
+
'Location data',
|
|
55
|
+
'Online identifiers (IP address, cookies)',
|
|
56
|
+
'Credentials (usernames, passwords)',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const handleChange = (
|
|
60
|
+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
|
61
|
+
) => {
|
|
62
|
+
const { name, value, type } = e.target as HTMLInputElement;
|
|
63
|
+
|
|
64
|
+
if (type === 'checkbox') {
|
|
65
|
+
setFormData((prev) => ({
|
|
66
|
+
...prev,
|
|
67
|
+
[name]: (e.target as HTMLInputElement).checked,
|
|
68
|
+
}));
|
|
69
|
+
} else if (type === 'number') {
|
|
70
|
+
setFormData((prev) => ({
|
|
71
|
+
...prev,
|
|
72
|
+
[name]: parseInt(value) || 0,
|
|
73
|
+
}));
|
|
74
|
+
} else {
|
|
75
|
+
setFormData((prev) => ({
|
|
76
|
+
...prev,
|
|
77
|
+
[name]: value,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Clear error when field is edited
|
|
82
|
+
if (errors[name]) {
|
|
83
|
+
setErrors((prev) => {
|
|
84
|
+
const newErrors = { ...prev };
|
|
85
|
+
delete newErrors[name];
|
|
86
|
+
return newErrors;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleDataCategoryToggle = (category: string) => {
|
|
92
|
+
setFormData((prev) => {
|
|
93
|
+
const updatedCategories = prev.dataCategories.includes(category)
|
|
94
|
+
? prev.dataCategories.filter((c) => c !== category)
|
|
95
|
+
: [...prev.dataCategories, category];
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...prev,
|
|
99
|
+
dataCategories: updatedCategories,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Clear error when field is edited
|
|
104
|
+
if (errors.dataCategories) {
|
|
105
|
+
setErrors((prev) => {
|
|
106
|
+
const newErrors = { ...prev };
|
|
107
|
+
delete newErrors.dataCategories;
|
|
108
|
+
return newErrors;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleAddDataCategory = (e?: React.MouseEvent) => {
|
|
114
|
+
// Prevent form submission if this is triggered by a button click
|
|
115
|
+
if (e) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (newDataCategory.trim()) {
|
|
120
|
+
// Check if category already exists to avoid duplicates
|
|
121
|
+
if (!formData.dataCategories.includes(newDataCategory.trim())) {
|
|
122
|
+
setFormData((prev) => ({
|
|
123
|
+
...prev,
|
|
124
|
+
dataCategories: [...prev.dataCategories, newDataCategory.trim()],
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
setNewDataCategory('');
|
|
128
|
+
|
|
129
|
+
// Clear error when field is edited
|
|
130
|
+
if (errors.dataCategories) {
|
|
131
|
+
setErrors((prev) => {
|
|
132
|
+
const newErrors = { ...prev };
|
|
133
|
+
delete newErrors.dataCategories;
|
|
134
|
+
return newErrors;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleAddMitigationStep = (e?: React.MouseEvent) => {
|
|
141
|
+
// Prevent form submission if this is triggered by a button click
|
|
142
|
+
if (e) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
}
|
|
145
|
+
if (newMitigationStep.trim()) {
|
|
146
|
+
setFormData((prev) => ({
|
|
147
|
+
...prev,
|
|
148
|
+
mitigationSteps: [...prev.mitigationSteps, newMitigationStep.trim()],
|
|
149
|
+
}));
|
|
150
|
+
setNewMitigationStep('');
|
|
151
|
+
|
|
152
|
+
// Clear error when field is edited
|
|
153
|
+
if (errors.mitigationSteps) {
|
|
154
|
+
setErrors((prev) => {
|
|
155
|
+
const newErrors = { ...prev };
|
|
156
|
+
delete newErrors.mitigationSteps;
|
|
157
|
+
return newErrors;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleRemoveMitigationStep = (index: number) => {
|
|
164
|
+
setFormData((prev) => ({
|
|
165
|
+
...prev,
|
|
166
|
+
mitigationSteps: prev.mitigationSteps.filter((_, i) => i !== index),
|
|
167
|
+
}));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Handle key press for adding data category and mitigation steps
|
|
171
|
+
const handleKeyPress = (e: React.KeyboardEvent, type: 'category' | 'mitigation') => {
|
|
172
|
+
if (e.key === 'Enter') {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
if (type === 'category') {
|
|
175
|
+
handleAddDataCategory();
|
|
176
|
+
} else {
|
|
177
|
+
handleAddMitigationStep();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const validateForm = () => {
|
|
183
|
+
const newErrors: Record<string, string> = {};
|
|
184
|
+
|
|
185
|
+
if (!formData.title.trim()) {
|
|
186
|
+
newErrors.title = 'Title is required';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!formData.description.trim()) {
|
|
190
|
+
newErrors.description = 'Description is required';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!formData.discoveryDate) {
|
|
194
|
+
newErrors.discoveryDate = 'Discovery date is required';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (formData.affectedDataSubjects <= 0) {
|
|
198
|
+
newErrors.affectedDataSubjects = 'Number of affected data subjects must be greater than 0';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (formData.dataCategories.length === 0) {
|
|
202
|
+
newErrors.dataCategories = 'Please select at least one data category';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (formData.mitigationSteps.length === 0) {
|
|
206
|
+
newErrors.mitigationSteps = 'Please add at least one mitigation step';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setErrors(newErrors);
|
|
210
|
+
return Object.keys(newErrors).length === 0;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
|
|
216
|
+
if (!validateForm()) return;
|
|
217
|
+
|
|
218
|
+
setIsSubmitting(true);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// In a real implementation, this would call an API
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
|
|
223
|
+
|
|
224
|
+
onSubmit({
|
|
225
|
+
title: formData.title,
|
|
226
|
+
description: formData.description,
|
|
227
|
+
discoveryDate: formData.discoveryDate,
|
|
228
|
+
affectedDataSubjects: formData.affectedDataSubjects,
|
|
229
|
+
dataCategories: formData.dataCategories,
|
|
230
|
+
severity: formData.severity,
|
|
231
|
+
mitigationSteps: formData.mitigationSteps,
|
|
232
|
+
reportedToAuthorities: formData.reportedToAuthorities,
|
|
233
|
+
reportedToDataSubjects: formData.reportedToDataSubjects,
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Error submitting breach notification:', error);
|
|
237
|
+
setErrors({
|
|
238
|
+
submit: 'An error occurred while submitting the breach notification. Please try again.',
|
|
239
|
+
});
|
|
240
|
+
} finally {
|
|
241
|
+
setIsSubmitting(false);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className={`bg-white dark:bg-gray-800 shadow-sm rounded-lg border border-gray-200 dark:border-gray-700 p-6 ${className}`}>
|
|
247
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Data Breach Notification Form</h3>
|
|
248
|
+
|
|
249
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
250
|
+
<FormField
|
|
251
|
+
id="title"
|
|
252
|
+
label="Breach Title"
|
|
253
|
+
required
|
|
254
|
+
error={errors.title}
|
|
255
|
+
>
|
|
256
|
+
<Input
|
|
257
|
+
id="title"
|
|
258
|
+
name="title"
|
|
259
|
+
value={formData.title}
|
|
260
|
+
onChange={handleChange}
|
|
261
|
+
placeholder="Enter title describing the breach"
|
|
262
|
+
/>
|
|
263
|
+
</FormField>
|
|
264
|
+
|
|
265
|
+
<FormField
|
|
266
|
+
id="description"
|
|
267
|
+
label="Breach Description"
|
|
268
|
+
required
|
|
269
|
+
error={errors.description}
|
|
270
|
+
>
|
|
271
|
+
<TextArea
|
|
272
|
+
id="description"
|
|
273
|
+
name="description"
|
|
274
|
+
value={formData.description}
|
|
275
|
+
onChange={handleChange}
|
|
276
|
+
rows={4}
|
|
277
|
+
placeholder="Detailed description of what happened, how it happened, and the potential impact"
|
|
278
|
+
/>
|
|
279
|
+
</FormField>
|
|
280
|
+
|
|
281
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
282
|
+
<FormField
|
|
283
|
+
id="discoveryDate"
|
|
284
|
+
label="Date of Discovery"
|
|
285
|
+
required
|
|
286
|
+
error={errors.discoveryDate}
|
|
287
|
+
>
|
|
288
|
+
<Input
|
|
289
|
+
type="date"
|
|
290
|
+
id="discoveryDate"
|
|
291
|
+
name="discoveryDate"
|
|
292
|
+
value={formData.discoveryDate}
|
|
293
|
+
onChange={handleChange}
|
|
294
|
+
max={new Date().toISOString().split('T')[0]}
|
|
295
|
+
/>
|
|
296
|
+
</FormField>
|
|
297
|
+
|
|
298
|
+
<FormField
|
|
299
|
+
id="affectedDataSubjects"
|
|
300
|
+
label="Number of Affected Data Subjects"
|
|
301
|
+
required
|
|
302
|
+
error={errors.affectedDataSubjects}
|
|
303
|
+
>
|
|
304
|
+
<Input
|
|
305
|
+
type="number"
|
|
306
|
+
id="affectedDataSubjects"
|
|
307
|
+
name="affectedDataSubjects"
|
|
308
|
+
value={formData.affectedDataSubjects}
|
|
309
|
+
onChange={handleChange}
|
|
310
|
+
min={0}
|
|
311
|
+
/>
|
|
312
|
+
</FormField>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<FormField
|
|
316
|
+
id="severity"
|
|
317
|
+
label="Breach Severity"
|
|
318
|
+
required
|
|
319
|
+
error={errors.severity}
|
|
320
|
+
>
|
|
321
|
+
<select
|
|
322
|
+
id="severity"
|
|
323
|
+
name="severity"
|
|
324
|
+
value={formData.severity}
|
|
325
|
+
onChange={handleChange}
|
|
326
|
+
className="block w-full px-3 py-2 text-base border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400 dark:focus:border-blue-400"
|
|
327
|
+
>
|
|
328
|
+
<option value="low">Low - Minimal impact on data subjects</option>
|
|
329
|
+
<option value="medium">Medium - Moderate impact on data subjects</option>
|
|
330
|
+
<option value="high">High - Significant impact on data subjects</option>
|
|
331
|
+
<option value="critical">Critical - Severe impact on data subjects</option>
|
|
332
|
+
</select>
|
|
333
|
+
</FormField>
|
|
334
|
+
|
|
335
|
+
<FormField
|
|
336
|
+
id="dataCategories"
|
|
337
|
+
label="Categories of Data Involved"
|
|
338
|
+
required
|
|
339
|
+
error={errors.dataCategories}
|
|
340
|
+
>
|
|
341
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
|
342
|
+
{formData.dataCategories.length > 0 && formData.dataCategories.filter(cat => !defaultDataCategories.includes(cat)).length > 0 && (
|
|
343
|
+
<div className="col-span-2 mb-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
344
|
+
<h4 className="text-sm font-medium mb-1">Added Categories:</h4>
|
|
345
|
+
<div className="flex flex-wrap gap-2">
|
|
346
|
+
{formData.dataCategories
|
|
347
|
+
.filter(cat => !defaultDataCategories.includes(cat))
|
|
348
|
+
.map((category, index) => (
|
|
349
|
+
<span key={index} className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
|
350
|
+
{category}
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
className="ml-1 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
|
354
|
+
onClick={() => {
|
|
355
|
+
setFormData(prev => ({
|
|
356
|
+
...prev,
|
|
357
|
+
dataCategories: prev.dataCategories.filter(c => c !== category)
|
|
358
|
+
}));
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
×
|
|
362
|
+
</button>
|
|
363
|
+
</span>
|
|
364
|
+
))}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
{defaultDataCategories.map((category) => (
|
|
369
|
+
<Checkbox
|
|
370
|
+
key={category}
|
|
371
|
+
id={`category-${category}`}
|
|
372
|
+
label={category}
|
|
373
|
+
checked={formData.dataCategories.includes(category)}
|
|
374
|
+
onChange={() => handleDataCategoryToggle(category)}
|
|
375
|
+
/>
|
|
376
|
+
))}
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div className="flex space-x-2 mt-4">
|
|
380
|
+
<Input
|
|
381
|
+
type="text"
|
|
382
|
+
id="newDataCategory"
|
|
383
|
+
value={newDataCategory}
|
|
384
|
+
onChange={(e) => setNewDataCategory(e.target.value)}
|
|
385
|
+
onKeyPress={(e) => handleKeyPress(e, 'category')}
|
|
386
|
+
placeholder="Add other data category"
|
|
387
|
+
className="flex-1"
|
|
388
|
+
/>
|
|
389
|
+
<Button
|
|
390
|
+
type="button"
|
|
391
|
+
onClick={(e) => handleAddDataCategory(e)}
|
|
392
|
+
size="sm"
|
|
393
|
+
>
|
|
394
|
+
Add
|
|
395
|
+
</Button>
|
|
396
|
+
</div>
|
|
397
|
+
</FormField>
|
|
398
|
+
|
|
399
|
+
<FormField
|
|
400
|
+
id="mitigationSteps"
|
|
401
|
+
label="Mitigation Steps Taken"
|
|
402
|
+
error={errors.mitigationSteps}
|
|
403
|
+
>
|
|
404
|
+
<div className="flex space-x-2 mb-2">
|
|
405
|
+
<Input
|
|
406
|
+
type="text"
|
|
407
|
+
id="newMitigationStep"
|
|
408
|
+
value={newMitigationStep}
|
|
409
|
+
onChange={(e) => setNewMitigationStep(e.target.value)}
|
|
410
|
+
onKeyPress={(e) => handleKeyPress(e, 'mitigation')}
|
|
411
|
+
placeholder="e.g., Changed all passwords, Patched vulnerability"
|
|
412
|
+
className="flex-1"
|
|
413
|
+
/>
|
|
414
|
+
<Button
|
|
415
|
+
type="button"
|
|
416
|
+
onClick={(e) => handleAddMitigationStep(e)}
|
|
417
|
+
size="sm"
|
|
418
|
+
>
|
|
419
|
+
Add
|
|
420
|
+
</Button>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{formData.mitigationSteps.length > 0 && (
|
|
424
|
+
<ul className="mt-2 space-y-1">
|
|
425
|
+
{formData.mitigationSteps.map((step, index) => (
|
|
426
|
+
<li key={index} className="flex items-center justify-between bg-gray-50 dark:bg-gray-700 p-2 rounded">
|
|
427
|
+
<span className="text-sm text-gray-700 dark:text-gray-300">{step}</span>
|
|
428
|
+
<Button
|
|
429
|
+
type="button"
|
|
430
|
+
onClick={() => handleRemoveMitigationStep(index)}
|
|
431
|
+
variant="ghost"
|
|
432
|
+
size="sm"
|
|
433
|
+
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
|
434
|
+
>
|
|
435
|
+
Remove
|
|
436
|
+
</Button>
|
|
437
|
+
</li>
|
|
438
|
+
))}
|
|
439
|
+
</ul>
|
|
440
|
+
)}
|
|
441
|
+
</FormField>
|
|
442
|
+
|
|
443
|
+
<div className="space-y-4">
|
|
444
|
+
<Checkbox
|
|
445
|
+
id="reportedToAuthorities"
|
|
446
|
+
name="reportedToAuthorities"
|
|
447
|
+
checked={formData.reportedToAuthorities}
|
|
448
|
+
onChange={handleChange}
|
|
449
|
+
label="Reported to Nigerian Data Protection Commission (NDPC)"
|
|
450
|
+
/>
|
|
451
|
+
|
|
452
|
+
<Checkbox
|
|
453
|
+
id="reportedToDataSubjects"
|
|
454
|
+
name="reportedToDataSubjects"
|
|
455
|
+
checked={formData.reportedToDataSubjects}
|
|
456
|
+
onChange={handleChange}
|
|
457
|
+
label="Reported to affected data subjects"
|
|
458
|
+
/>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
{errors.submit && (
|
|
462
|
+
<div className="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
|
|
463
|
+
<p className="text-sm text-red-700 dark:text-red-400">{errors.submit}</p>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
<div className="flex justify-end">
|
|
468
|
+
<Button
|
|
469
|
+
type="submit"
|
|
470
|
+
disabled={isSubmitting}
|
|
471
|
+
variant="default"
|
|
472
|
+
>
|
|
473
|
+
{isSubmitting ? 'Submitting...' : 'Submit Breach Notification'}
|
|
474
|
+
</Button>
|
|
475
|
+
</div>
|
|
476
|
+
</form>
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ConsentOption } from '@/types';
|
|
5
|
+
import { Button } from '@/components/ui/Button';
|
|
6
|
+
import { Checkbox } from '@/components/ui/Checkbox';
|
|
7
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from '@/components/ui/Card';
|
|
8
|
+
import { Badge } from '@/components/ui/Badge';
|
|
9
|
+
|
|
10
|
+
interface ConsentBannerProps {
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
privacyPolicyUrl?: string;
|
|
14
|
+
options: ConsentOption[];
|
|
15
|
+
onSave: (consents: Record<string, boolean>) => void;
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function ConsentBanner({
|
|
21
|
+
title = 'Privacy Preferences',
|
|
22
|
+
description = 'We use cookies and similar technologies to provide certain features, enhance the user experience and deliver content that is relevant to your interests.',
|
|
23
|
+
privacyPolicyUrl = '/privacy-policy',
|
|
24
|
+
options,
|
|
25
|
+
onSave,
|
|
26
|
+
className = '',
|
|
27
|
+
}: ConsentBannerProps) {
|
|
28
|
+
const [consents, setConsents] = useState<Record<string, boolean>>(
|
|
29
|
+
options.reduce((acc, option) => {
|
|
30
|
+
acc[option.id] = option.defaultValue ?? false;
|
|
31
|
+
return acc;
|
|
32
|
+
}, {} as Record<string, boolean>)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const handleToggle = (id: string) => {
|
|
36
|
+
const option = options.find(opt => opt.id === id);
|
|
37
|
+
if (option?.required) return;
|
|
38
|
+
|
|
39
|
+
setConsents(prev => ({
|
|
40
|
+
...prev,
|
|
41
|
+
[id]: !prev[id]
|
|
42
|
+
}));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleAcceptAll = () => {
|
|
46
|
+
const allConsents = options.reduce((acc, option) => {
|
|
47
|
+
acc[option.id] = true;
|
|
48
|
+
return acc;
|
|
49
|
+
}, {} as Record<string, boolean>);
|
|
50
|
+
|
|
51
|
+
setConsents(allConsents);
|
|
52
|
+
onSave(allConsents);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleSave = () => {
|
|
56
|
+
onSave(consents);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleRejectAll = () => {
|
|
60
|
+
const rejectedConsents = options.reduce((acc, option) => {
|
|
61
|
+
acc[option.id] = option.required ? true : false;
|
|
62
|
+
return acc;
|
|
63
|
+
}, {} as Record<string, boolean>);
|
|
64
|
+
|
|
65
|
+
setConsents(rejectedConsents);
|
|
66
|
+
onSave(rejectedConsents);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={`fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-black/50 ${className}`}>
|
|
71
|
+
<Card className="w-full max-w-3xl max-h-[90vh] overflow-auto shadow-xl animate-in fade-in slide-in-from-bottom-5 duration-300">
|
|
72
|
+
<CardHeader className="border-b border-gray-200 dark:border-gray-700">
|
|
73
|
+
<div className="flex items-center justify-between">
|
|
74
|
+
<div>
|
|
75
|
+
<CardTitle className="text-xl">{title}</CardTitle>
|
|
76
|
+
<CardDescription className="mt-2">{description}</CardDescription>
|
|
77
|
+
</div>
|
|
78
|
+
<Badge variant="primary" className="hidden sm:flex">Privacy Settings</Badge>
|
|
79
|
+
</div>
|
|
80
|
+
<a
|
|
81
|
+
href={privacyPolicyUrl}
|
|
82
|
+
className="text-sm text-blue-600 dark:text-blue-400 hover:underline mt-2 inline-flex items-center"
|
|
83
|
+
>
|
|
84
|
+
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
85
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
86
|
+
</svg>
|
|
87
|
+
View our Privacy Policy
|
|
88
|
+
</a>
|
|
89
|
+
</CardHeader>
|
|
90
|
+
|
|
91
|
+
<CardContent className="space-y-4 p-6">
|
|
92
|
+
<div className="grid gap-4">
|
|
93
|
+
{options.map((option) => (
|
|
94
|
+
<div
|
|
95
|
+
key={option.id}
|
|
96
|
+
className={`p-4 rounded-lg border ${consents[option.id] ? 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20' : 'border-gray-200 dark:border-gray-700'} transition-colors duration-200`}
|
|
97
|
+
>
|
|
98
|
+
<div className="flex items-start">
|
|
99
|
+
<div className="flex-1">
|
|
100
|
+
<div className="flex items-center">
|
|
101
|
+
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
|
|
102
|
+
{option.label}
|
|
103
|
+
</h3>
|
|
104
|
+
{option.required && (
|
|
105
|
+
<Badge variant="secondary" className="ml-2 text-xs">Required</Badge>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
109
|
+
{option.description}
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="ml-3 flex h-5 items-center">
|
|
113
|
+
<Checkbox
|
|
114
|
+
id={`consent-${option.id}`}
|
|
115
|
+
checked={consents[option.id]}
|
|
116
|
+
onChange={() => handleToggle(option.id)}
|
|
117
|
+
disabled={option.required}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</CardContent>
|
|
125
|
+
|
|
126
|
+
<CardFooter className="flex flex-col sm:flex-row sm:justify-between border-t border-gray-200 dark:border-gray-700 p-6 bg-gray-50 dark:bg-gray-800">
|
|
127
|
+
<div className="mb-4 sm:mb-0">
|
|
128
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
129
|
+
You can change your preferences at any time by visiting your account settings.
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
|
|
133
|
+
<Button
|
|
134
|
+
variant="outline"
|
|
135
|
+
onClick={handleRejectAll}
|
|
136
|
+
className="sm:order-1"
|
|
137
|
+
>
|
|
138
|
+
Reject All
|
|
139
|
+
</Button>
|
|
140
|
+
<Button
|
|
141
|
+
variant="secondary"
|
|
142
|
+
onClick={handleSave}
|
|
143
|
+
className="sm:order-2"
|
|
144
|
+
>
|
|
145
|
+
Save Preferences
|
|
146
|
+
</Button>
|
|
147
|
+
<Button
|
|
148
|
+
variant="default"
|
|
149
|
+
onClick={handleAcceptAll}
|
|
150
|
+
className="sm:order-3"
|
|
151
|
+
>
|
|
152
|
+
Accept All
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
</CardFooter>
|
|
156
|
+
</Card>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|