@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,201 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ interface LoadingSpinnerProps {
7
+ size?: 'sm' | 'md' | 'lg';
8
+ className?: string;
9
+ }
10
+
11
+ export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
12
+ const sizeClasses = {
13
+ sm: 'h-4 w-4',
14
+ md: 'h-8 w-8',
15
+ lg: 'h-12 w-12',
16
+ };
17
+
18
+ return (
19
+ <div
20
+ className={cn('inline-block animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]', sizeClasses[size], className)}
21
+ role="status"
22
+ aria-label="Loading"
23
+ >
24
+ <span className="sr-only">Loading...</span>
25
+ </div>
26
+ );
27
+ }
28
+
29
+ interface LoadingOverlayProps {
30
+ children?: React.ReactNode;
31
+ message?: string;
32
+ }
33
+
34
+ export function LoadingOverlay({ children, message = 'Loading...' }: LoadingOverlayProps) {
35
+ return (
36
+ <div className="relative">
37
+ <div className="absolute inset-0 bg-white/75 dark:bg-gray-900/75 backdrop-blur-sm z-10 flex items-center justify-center">
38
+ <div className="text-center">
39
+ <LoadingSpinner size="lg" className="mx-auto mb-4 text-blue-600 dark:text-blue-400" />
40
+ <p className="text-sm font-medium text-gray-700 dark:text-gray-300">{message}</p>
41
+ </div>
42
+ </div>
43
+ {children}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ interface LoadingSkeletonProps {
49
+ className?: string;
50
+ lines?: number;
51
+ }
52
+
53
+ export function LoadingSkeleton({ className, lines = 1 }: LoadingSkeletonProps) {
54
+ return (
55
+ <div className={cn('animate-pulse', className)}>
56
+ {Array.from({ length: lines }).map((_, i) => (
57
+ <div
58
+ key={i}
59
+ className={cn(
60
+ 'h-4 bg-gray-200 dark:bg-gray-700 rounded',
61
+ i !== lines - 1 && 'mb-2',
62
+ i === lines - 1 && 'w-3/4'
63
+ )}
64
+ />
65
+ ))}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ interface LoadingButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
71
+ loading?: boolean;
72
+ loadingText?: string;
73
+ children: React.ReactNode;
74
+ }
75
+
76
+ export function LoadingButton({
77
+ loading = false,
78
+ loadingText = 'Loading...',
79
+ children,
80
+ disabled,
81
+ className,
82
+ ...props
83
+ }: LoadingButtonProps) {
84
+ return (
85
+ <button
86
+ disabled={loading || disabled}
87
+ className={cn(
88
+ 'relative inline-flex items-center justify-center',
89
+ loading && 'cursor-not-allowed opacity-70',
90
+ className
91
+ )}
92
+ {...props}
93
+ >
94
+ {loading ? (
95
+ <>
96
+ <LoadingSpinner size="sm" className="mr-2" />
97
+ <span>{loadingText}</span>
98
+ </>
99
+ ) : (
100
+ children
101
+ )}
102
+ </button>
103
+ );
104
+ }
105
+
106
+ interface LoadingStateProps {
107
+ loading: boolean;
108
+ error?: Error | null;
109
+ empty?: boolean;
110
+ children: React.ReactNode;
111
+ loadingComponent?: React.ReactNode;
112
+ errorComponent?: React.ReactNode;
113
+ emptyComponent?: React.ReactNode;
114
+ emptyMessage?: string;
115
+ }
116
+
117
+ export function LoadingState({
118
+ loading,
119
+ error,
120
+ empty,
121
+ children,
122
+ loadingComponent,
123
+ errorComponent,
124
+ emptyComponent,
125
+ emptyMessage = 'No data found',
126
+ }: LoadingStateProps) {
127
+ if (loading) {
128
+ return (
129
+ <>
130
+ {loadingComponent || (
131
+ <div className="flex items-center justify-center py-8">
132
+ <LoadingSpinner size="lg" />
133
+ </div>
134
+ )}
135
+ </>
136
+ );
137
+ }
138
+
139
+ if (error) {
140
+ return (
141
+ <>
142
+ {errorComponent || (
143
+ <div className="rounded-md bg-red-50 dark:bg-red-900/20 p-4" role="alert">
144
+ <div className="flex">
145
+ <div className="flex-shrink-0">
146
+ <svg
147
+ className="h-5 w-5 text-red-400"
148
+ viewBox="0 0 20 20"
149
+ fill="currentColor"
150
+ aria-hidden="true"
151
+ >
152
+ <path
153
+ fillRule="evenodd"
154
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
155
+ clipRule="evenodd"
156
+ />
157
+ </svg>
158
+ </div>
159
+ <div className="ml-3">
160
+ <h3 className="text-sm font-medium text-red-800 dark:text-red-200">
161
+ Error loading data
162
+ </h3>
163
+ <p className="mt-1 text-sm text-red-700 dark:text-red-300">
164
+ {error.message}
165
+ </p>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ )}
170
+ </>
171
+ );
172
+ }
173
+
174
+ if (empty) {
175
+ return (
176
+ <>
177
+ {emptyComponent || (
178
+ <div className="text-center py-8">
179
+ <svg
180
+ className="mx-auto h-12 w-12 text-gray-400"
181
+ fill="none"
182
+ viewBox="0 0 24 24"
183
+ stroke="currentColor"
184
+ aria-hidden="true"
185
+ >
186
+ <path
187
+ strokeLinecap="round"
188
+ strokeLinejoin="round"
189
+ strokeWidth={2}
190
+ d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
191
+ />
192
+ </svg>
193
+ <p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{emptyMessage}</p>
194
+ </div>
195
+ )}
196
+ </>
197
+ );
198
+ }
199
+
200
+ return <>{children}</>;
201
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ // This interface extends the standard HTML select attributes
7
+ // and allows for additional props to be added in the future
8
+ /* eslint-disable-next-line @typescript-eslint/no-empty-object-type */
9
+ export interface SelectProps
10
+ extends React.SelectHTMLAttributes<HTMLSelectElement> {
11
+ // Custom props can be added here
12
+ }
13
+
14
+ const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
15
+ ({ className, children, ...props }, ref) => {
16
+ return (
17
+ <select
18
+ className={cn(
19
+ "block w-full px-3 py-2 text-base",
20
+ "border border-gray-300 dark:border-gray-600 rounded-md shadow-sm",
21
+ "bg-white dark:bg-gray-800 text-gray-900 dark:text-white",
22
+ "placeholder:text-gray-400 dark:placeholder:text-gray-500",
23
+ "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400 dark:focus:border-blue-400",
24
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-50 dark:disabled:bg-gray-900",
25
+ "appearance-none bg-no-repeat bg-right",
26
+ "bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20stroke%3D%22%236b7280%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%221.5%22%20d%3D%22m6%208%204%204%204-4%22%2F%3E%3C%2Fsvg%3E')]",
27
+ "bg-[length:1.25em_1.25em] pr-10",
28
+ "hover:border-blue-400 dark:hover:border-blue-500 transition-colors duration-200",
29
+ className
30
+ )}
31
+ ref={ref}
32
+ {...props}
33
+ >
34
+ {children}
35
+ </select>
36
+ );
37
+ }
38
+ );
39
+
40
+ Select.displayName = "Select";
41
+
42
+ export { Select };
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ // This interface extends the standard HTML textarea attributes
7
+ // and allows for additional props to be added in the future
8
+ /* eslint-disable-next-line @typescript-eslint/no-empty-object-type */
9
+ export interface TextAreaProps
10
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
11
+ // Custom props can be added here
12
+ }
13
+
14
+ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
15
+ ({ className, ...props }, ref) => {
16
+ return (
17
+ <textarea
18
+ className={cn(
19
+ "block w-full px-3 py-2 text-base",
20
+ "border border-gray-300 dark:border-gray-600 rounded-md shadow-sm",
21
+ "bg-white dark:bg-gray-800 text-gray-900 dark:text-white",
22
+ "placeholder:text-gray-400 dark:placeholder:text-gray-500",
23
+ "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400 dark:focus:border-blue-400",
24
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-50 dark:disabled:bg-gray-900",
25
+ "min-h-[80px] resize-y",
26
+ "hover:border-blue-400 dark:hover:border-blue-500 transition-colors duration-200",
27
+ className
28
+ )}
29
+ ref={ref}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+ );
35
+
36
+ TextArea.displayName = "TextArea";
37
+
38
+ export { TextArea };
@@ -0,0 +1,24 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SwitchPrimitive from "@radix-ui/react-switch"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Switch({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
12
+ return (
13
+ <SwitchPrimitive.Root
14
+ data-slot="switch"
15
+ className={cn(
16
+ "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
17
+ className
18
+ )}
19
+ {...props}
20
+ >
21
+ <SwitchPrimitive.Thumb
22
+ data-slot="switch-thumb"
23
+ className={cn(
24
+ "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
25
+ )}
26
+ />
27
+ </SwitchPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ export { Switch }
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Tabs({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
12
+ return (
13
+ <TabsPrimitive.Root
14
+ data-slot="tabs"
15
+ className={cn("flex flex-col gap-2", className)}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function TabsList({
22
+ className,
23
+ ...props
24
+ }: React.ComponentProps<typeof TabsPrimitive.List>) {
25
+ return (
26
+ <TabsPrimitive.List
27
+ data-slot="tabs-list"
28
+ className={cn(
29
+ "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
30
+ className
31
+ )}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function TabsTrigger({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
41
+ return (
42
+ <TabsPrimitive.Trigger
43
+ data-slot="tabs-trigger"
44
+ className={cn(
45
+ "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ function TabsContent({
54
+ className,
55
+ ...props
56
+ }: React.ComponentProps<typeof TabsPrimitive.Content>) {
57
+ return (
58
+ <TabsPrimitive.Content
59
+ data-slot="tabs-content"
60
+ className={cn("flex-1 outline-none", className)}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { ConsentRecord } from '@/types';
5
+ import consentService from '@/lib/consentService';
6
+
7
+ export function useConsent() {
8
+ const [consentRecord, setConsentRecord] = useState<ConsentRecord | null>(null);
9
+ const [isLoading, setIsLoading] = useState(true);
10
+ const [showBanner, setShowBanner] = useState(false);
11
+
12
+ useEffect(() => {
13
+ // Load consent from storage on component mount
14
+ const storedConsent = consentService.getCurrentConsent();
15
+ setConsentRecord(storedConsent);
16
+ setIsLoading(false);
17
+
18
+ // If no consent is stored, show the banner
19
+ if (!storedConsent) {
20
+ setShowBanner(true);
21
+ }
22
+ }, []);
23
+
24
+ const saveConsent = (consents: Record<string, boolean>, userId?: string) => {
25
+ const newRecord = consentService.saveConsent(consents, userId);
26
+ setConsentRecord(newRecord);
27
+ setShowBanner(false);
28
+ return newRecord;
29
+ };
30
+
31
+ const updateConsent = (
32
+ consents: Record<string, boolean>,
33
+ changeReason?: string,
34
+ userId?: string
35
+ ) => {
36
+ const updatedRecord = consentService.updateConsent(consents, changeReason, userId);
37
+ setConsentRecord(updatedRecord);
38
+ return updatedRecord;
39
+ };
40
+
41
+ const hasConsent = (type: string): boolean => {
42
+ if (!consentRecord) return false;
43
+ return consentRecord.consents[type] === true;
44
+ };
45
+
46
+ const openPreferences = () => {
47
+ setShowBanner(true);
48
+ };
49
+
50
+ const closePreferences = () => {
51
+ setShowBanner(false);
52
+ };
53
+
54
+ return {
55
+ consentRecord,
56
+ isLoading,
57
+ showBanner,
58
+ saveConsent,
59
+ updateConsent,
60
+ hasConsent,
61
+ openPreferences,
62
+ closePreferences
63
+ };
64
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+
5
+ interface LoadingState<T = unknown> {
6
+ loading: boolean;
7
+ error: Error | null;
8
+ data: T | null;
9
+ }
10
+
11
+ interface UseLoadingStateReturn<T = unknown> extends LoadingState<T> {
12
+ setLoading: (loading: boolean) => void;
13
+ setError: (error: Error | null) => void;
14
+ setData: (data: T | null) => void;
15
+ reset: () => void;
16
+ execute: <R = T>(asyncFunction: () => Promise<R>) => Promise<R | null>;
17
+ }
18
+
19
+ export function useLoadingState<T = unknown>(
20
+ initialData: T | null = null,
21
+ ): UseLoadingStateReturn<T> {
22
+ const [state, setState] = useState<LoadingState<T>>({
23
+ loading: false,
24
+ error: null,
25
+ data: initialData,
26
+ });
27
+
28
+ const setLoading = useCallback((loading: boolean) => {
29
+ setState((prev) => ({ ...prev, loading }));
30
+ }, []);
31
+
32
+ const setError = useCallback((error: Error | null) => {
33
+ setState((prev) => ({ ...prev, error, loading: false }));
34
+ }, []);
35
+
36
+ const setData = useCallback((data: T | null) => {
37
+ setState((prev) => ({ ...prev, data, loading: false, error: null }));
38
+ }, []);
39
+
40
+ const reset = useCallback(() => {
41
+ setState({
42
+ loading: false,
43
+ error: null,
44
+ data: initialData,
45
+ });
46
+ }, [initialData]);
47
+
48
+ const execute = useCallback(
49
+ async <R = T>(asyncFunction: () => Promise<R>): Promise<R | null> => {
50
+ try {
51
+ setLoading(true);
52
+ const result = await asyncFunction();
53
+ setData(result as unknown as T);
54
+ return result;
55
+ } catch (error) {
56
+ setError(error as Error);
57
+ return null;
58
+ }
59
+ },
60
+ [setLoading, setData, setError],
61
+ );
62
+
63
+ return {
64
+ ...state,
65
+ setLoading,
66
+ setError,
67
+ setData,
68
+ reset,
69
+ execute,
70
+ };
71
+ }
72
+
73
+ // Hook for managing multiple loading states
74
+ export function useMultipleLoadingStates<T extends Record<string, unknown>>(
75
+ keys: (keyof T)[],
76
+ ): Record<keyof T, UseLoadingStateReturn> {
77
+ const states: Partial<Record<keyof T, UseLoadingStateReturn>> = {};
78
+
79
+ keys.forEach((key) => {
80
+ // eslint-disable-next-line react-hooks/rules-of-hooks
81
+ states[key] = useLoadingState();
82
+ });
83
+
84
+ return states as Record<keyof T, UseLoadingStateReturn>;
85
+ }
@@ -0,0 +1,137 @@
1
+ 'use client';
2
+
3
+ import { ConsentRecord, ConsentHistoryEntry } from '@/types';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import { storage } from './storage';
6
+
7
+ // In a real implementation, this would connect to a database
8
+ // For demo purposes, we're using localStorage
9
+
10
+ const CONSENT_STORAGE_KEY = 'ndpr_consent_records';
11
+ const CONSENT_HISTORY_KEY = 'ndpr_consent_history';
12
+
13
+ // Helper function to get consent history
14
+ const getConsentHistoryHelper = (): ConsentHistoryEntry[] => {
15
+ return storage.getItem<ConsentHistoryEntry[]>(CONSENT_HISTORY_KEY, []) || [];
16
+ };
17
+
18
+ export const consentService = {
19
+ // Save a new consent record
20
+ saveConsent: (consents: Record<string, boolean>, userId?: string): ConsentRecord => {
21
+ const consentRecord: ConsentRecord = {
22
+ id: uuidv4(),
23
+ userId,
24
+ consents,
25
+ timestamp: new Date(),
26
+ ipAddress: 'Collected server-side in real implementation',
27
+ userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'Unknown',
28
+ version: '1.0',
29
+ };
30
+
31
+ // Store in localStorage for demo
32
+ // Save as current consent
33
+ if (!storage.setItem(CONSENT_STORAGE_KEY, consentRecord)) {
34
+ throw new Error('Failed to save consent record');
35
+ }
36
+
37
+ // Add to history
38
+ const historyEntry: ConsentHistoryEntry = {
39
+ timestamp: new Date(),
40
+ consents,
41
+ action: 'granted',
42
+ ipAddress: 'Collected server-side in real implementation',
43
+ userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'Unknown',
44
+ version: '1.0',
45
+ };
46
+
47
+ const history = getConsentHistoryHelper();
48
+ history.push(historyEntry);
49
+ if (!storage.setItem(CONSENT_HISTORY_KEY, history)) {
50
+ throw new Error('Failed to save consent history');
51
+ }
52
+
53
+ return consentRecord;
54
+ },
55
+
56
+ // Get the current consent record
57
+ getCurrentConsent: (): ConsentRecord | null => {
58
+ return storage.getItem<ConsentRecord>(CONSENT_STORAGE_KEY);
59
+ },
60
+
61
+ // Get consent history
62
+ getConsentHistory: (): ConsentHistoryEntry[] => {
63
+ return getConsentHistoryHelper();
64
+ },
65
+
66
+ // Update consent with change reason
67
+ updateConsent: (
68
+ consents: Record<string, boolean>,
69
+ changeReason?: string,
70
+ userId?: string
71
+ ): ConsentRecord => {
72
+ // Get the previous consent to determine the action
73
+ const previousConsent = consentService.getCurrentConsent();
74
+
75
+ // Create the new consent record
76
+ const consentRecord: ConsentRecord = {
77
+ id: uuidv4(),
78
+ userId,
79
+ consents,
80
+ timestamp: new Date(),
81
+ ipAddress: 'Collected server-side in real implementation',
82
+ userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'Unknown',
83
+ version: '1.0',
84
+ };
85
+
86
+ // Store in localStorage for demo
87
+ // Save as current consent
88
+ if (!storage.setItem(CONSENT_STORAGE_KEY, consentRecord)) {
89
+ throw new Error('Failed to save consent record');
90
+ }
91
+
92
+ // Determine the action type
93
+ let action: 'granted' | 'revoked' | 'updated' = 'updated';
94
+ if (!previousConsent) {
95
+ action = 'granted';
96
+ } else {
97
+ const allRevoked = Object.values(consents).every(v => !v);
98
+ if (allRevoked) {
99
+ action = 'revoked';
100
+ }
101
+ }
102
+
103
+ // Add to history
104
+ const historyEntry: ConsentHistoryEntry = {
105
+ timestamp: new Date(),
106
+ consents,
107
+ action,
108
+ ipAddress: 'Collected server-side in real implementation',
109
+ userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'Unknown',
110
+ version: '1.0',
111
+ };
112
+
113
+ const history = getConsentHistoryHelper();
114
+ history.push(historyEntry);
115
+ if (!storage.setItem(CONSENT_HISTORY_KEY, history)) {
116
+ throw new Error('Failed to save consent history');
117
+ }
118
+
119
+ return consentRecord;
120
+ },
121
+
122
+ // Clear all consent data (for testing/development)
123
+ clearConsentData: (): boolean => {
124
+ const clearedCurrent = storage.removeItem(CONSENT_STORAGE_KEY);
125
+ const clearedHistory = storage.removeItem(CONSENT_HISTORY_KEY);
126
+ return clearedCurrent && clearedHistory;
127
+ },
128
+
129
+ // Check if a specific consent is granted
130
+ hasConsent: (type: string): boolean => {
131
+ const current = consentService.getCurrentConsent();
132
+ if (!current) return false;
133
+ return current.consents[type] === true;
134
+ }
135
+ };
136
+
137
+ export default consentService;