@redacto.io/consent-sdk-react 4.0.0 → 5.1.0

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 (161) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/LICENSE +176 -0
  3. package/README.md +98 -1
  4. package/dist/index.js +231 -96
  5. package/dist/index.mjs +231 -96
  6. package/dist/privacy-center.d.mts +649 -0
  7. package/dist/privacy-center.d.ts +649 -0
  8. package/dist/privacy-center.js +20824 -0
  9. package/dist/privacy-center.mjs +20789 -0
  10. package/package.json +47 -3
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +6 -4
  12. package/src/RedactoNoticeConsent/api/index.ts +2 -0
  13. package/src/RedactoNoticeConsent/api/types.ts +2 -0
  14. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +128 -62
  15. package/src/RedactoNoticeConsent/language-codes.ts +97 -0
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
  17. package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
  18. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
  19. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
  20. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
  21. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
  22. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +30 -0
  23. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +173 -0
  24. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +55 -0
  25. package/src/RedactoPrivacyCenter/api/actions.ts +476 -0
  26. package/src/RedactoPrivacyCenter/api/client.ts +524 -0
  27. package/src/RedactoPrivacyCenter/api/config.ts +33 -0
  28. package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
  29. package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
  30. package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
  31. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
  32. package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
  33. package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
  34. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
  35. package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
  36. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
  37. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
  38. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
  39. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
  40. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
  41. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
  42. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
  43. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
  44. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
  45. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
  46. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
  47. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
  48. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
  49. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +469 -0
  50. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +426 -0
  51. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +732 -0
  52. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +238 -0
  53. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
  54. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
  55. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
  56. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
  57. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
  58. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +304 -0
  59. package/src/RedactoPrivacyCenter/components/Form.tsx +1596 -0
  60. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +174 -0
  61. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +337 -0
  62. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +330 -0
  63. package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
  64. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +304 -0
  65. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +232 -0
  66. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
  67. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +408 -0
  68. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
  69. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
  70. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
  71. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
  72. package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
  73. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
  74. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +159 -0
  75. package/src/RedactoPrivacyCenter/components/constant.ts +16 -0
  76. package/src/RedactoPrivacyCenter/components/types.ts +76 -0
  77. package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
  78. package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
  79. package/src/RedactoPrivacyCenter/config/table.ts +25 -0
  80. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +137 -0
  81. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +41 -0
  82. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
  83. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +96 -0
  84. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
  85. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
  86. package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
  87. package/src/RedactoPrivacyCenter/context/types.ts +14 -0
  88. package/src/RedactoPrivacyCenter/lib/api.ts +566 -0
  89. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
  90. package/src/RedactoPrivacyCenter/lib/constants.ts +161 -0
  91. package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
  92. package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
  93. package/src/RedactoPrivacyCenter/lib/queries.ts +134 -0
  94. package/src/RedactoPrivacyCenter/lib/types.ts +531 -0
  95. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
  96. package/src/RedactoPrivacyCenter/lib/utils.ts +626 -0
  97. package/src/RedactoPrivacyCenter/locales/as/translation.json +280 -0
  98. package/src/RedactoPrivacyCenter/locales/bho/translation.json +280 -0
  99. package/src/RedactoPrivacyCenter/locales/bn/translation.json +280 -0
  100. package/src/RedactoPrivacyCenter/locales/brx/translation.json +280 -0
  101. package/src/RedactoPrivacyCenter/locales/doi/translation.json +280 -0
  102. package/src/RedactoPrivacyCenter/locales/en/translation.json +255 -0
  103. package/src/RedactoPrivacyCenter/locales/gom/translation.json +280 -0
  104. package/src/RedactoPrivacyCenter/locales/gu/translation.json +280 -0
  105. package/src/RedactoPrivacyCenter/locales/hi/translation.json +280 -0
  106. package/src/RedactoPrivacyCenter/locales/kn/translation.json +280 -0
  107. package/src/RedactoPrivacyCenter/locales/ks/translation.json +280 -0
  108. package/src/RedactoPrivacyCenter/locales/mai/translation.json +280 -0
  109. package/src/RedactoPrivacyCenter/locales/ml/translation.json +280 -0
  110. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +280 -0
  111. package/src/RedactoPrivacyCenter/locales/mr/translation.json +280 -0
  112. package/src/RedactoPrivacyCenter/locales/ne/translation.json +280 -0
  113. package/src/RedactoPrivacyCenter/locales/or/translation.json +280 -0
  114. package/src/RedactoPrivacyCenter/locales/pa/translation.json +280 -0
  115. package/src/RedactoPrivacyCenter/locales/sa/translation.json +280 -0
  116. package/src/RedactoPrivacyCenter/locales/sat/translation.json +280 -0
  117. package/src/RedactoPrivacyCenter/locales/sd/translation.json +280 -0
  118. package/src/RedactoPrivacyCenter/locales/ta/translation.json +280 -0
  119. package/src/RedactoPrivacyCenter/locales/te/translation.json +280 -0
  120. package/src/RedactoPrivacyCenter/locales/ur/translation.json +280 -0
  121. package/src/RedactoPrivacyCenter/styles/base.css +117 -0
  122. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1725 -0
  123. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +813 -0
  124. package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
  125. package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
  126. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +367 -0
  127. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +476 -0
  128. package/src/RedactoPrivacyCenter/table/index.ts +2 -0
  129. package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
  130. package/src/RedactoPrivacyCenter/ui/Footer.tsx +75 -0
  131. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
  132. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +93 -0
  133. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
  134. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
  135. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
  136. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
  137. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
  138. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +107 -0
  139. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
  140. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +132 -0
  141. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +31 -0
  142. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +106 -0
  143. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +173 -0
  144. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +387 -0
  145. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +149 -0
  146. package/src/RedactoPrivacyCenter/ui/PCText.tsx +41 -0
  147. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
  148. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
  149. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
  150. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
  151. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +53 -0
  152. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +72 -0
  153. package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
  154. package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
  155. package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
  156. package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
  157. package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
  158. package/src/privacy-center.ts +108 -0
  159. package/tsconfig.json +2 -0
  160. package/tsup.config.ts +16 -1
  161. package/.turbo/turbo-build.log +0 -21
@@ -0,0 +1,1596 @@
1
+ import fileAttachIcon from '../assets/fileAttach.svg';
2
+ import uploadFileIcon from '../assets/uploadFileIcon.svg';
3
+ import { uploadFileWithS2S } from '../api/upload';
4
+ import { FormBlockWrapper } from '../ui/FormBlockWrapper';
5
+ import { useQueryClient } from '@tanstack/react-query';
6
+ import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { AiOutlineClose } from 'react-icons/ai';
9
+ import { PiUploadBold } from 'react-icons/pi';
10
+ import { toast } from '../utils/toast';
11
+ import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCTextarea } from '../ui';
12
+ import { PCModal } from '../ui/PCModal';
13
+ import { pcStyles } from '../styles/pcStyles';
14
+ import { createCaseRequest, getFormData } from '../lib/api';
15
+ import { ensureValidToken } from '../lib/utils';
16
+ import { useAuth } from '../context/AuthContext';
17
+ import { useBaseUrl } from '../context/BaseUrlContext';
18
+ import {
19
+ useRequestTypeLabels,
20
+ useTimePeriodOptions,
21
+ } from '../lib/constants';
22
+ import { useOrganization } from '../context/OrganizationContext';
23
+ import i18n from '../lib/i18n';
24
+ import { getLangParam } from '../lib/useCurrentLanguage';
25
+ import type {
26
+ DataElement,
27
+ FormData,
28
+ GrievanceOptions,
29
+ GrievanceTypeElement,
30
+ GrievanceValue,
31
+ NominationData,
32
+ Purpose,
33
+ UpdateCorrectionDataItem,
34
+ UpdatePurpose,
35
+ UserDataRequest,
36
+ } from '../lib/types';
37
+ import { RequestTypeEnum } from '../lib/types';
38
+ import CorrectionDataSection from './DataRequest/CorrectionDataSection';
39
+ import GrievanceDataSection from './DataRequest/GrievanceDataSection';
40
+ import NominationDataSection from './DataRequest/NominationDataSection';
41
+ import PurposeSelectionSection from './DataRequest/PurposeSelectionSection';
42
+ import RequestSubmit from './RequestSubmit';
43
+ import type { CaseHistoryProps } from './types';
44
+ import { FiAlertCircle, FiChevronDown, FiChevronUp, FiX } from 'react-icons/fi';
45
+ import { MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL } from './constant';
46
+
47
+ const getVar = (varName: string): string => `var(${varName})`;
48
+
49
+ export default function Form({
50
+ onCreateRequest,
51
+ isPopup = false,
52
+ onClose,
53
+ }: CaseHistoryProps) {
54
+ const queryClient = useQueryClient();
55
+ const timePeriodOptions = useTimePeriodOptions();
56
+ const { organization } = useOrganization();
57
+ const orgSlug = organization?.slug || organization?.name || '';
58
+
59
+ // Detect if we're in case management context (admin side)
60
+ // Case management uses session auth, not JWT Bearer
61
+ // We rely on an explicit pathname check rather than absence of access_token
62
+ // to avoid incorrectly flagging unauthenticated privacy-center users as case-management
63
+ const isCaseManagementContext =
64
+ typeof window !== 'undefined' &&
65
+ window.location.pathname.includes('/case-management');
66
+ const [requestType, setRequestType] = useState<RequestTypeEnum | null>(null);
67
+ const [timePeriod, setTimePeriod] = useState('');
68
+ const [isSubmitted, setIsSubmitted] = useState(false);
69
+ const { t } = useTranslation();
70
+ const { accessToken, refreshToken, onError, updateTokens } = useAuth();
71
+ const { consentClient, baseUrl } = useBaseUrl();
72
+ const organizationId = organization?.uuid;
73
+ const workspaceId = organization?.workspaces[0]?.uuid;
74
+
75
+ // Create auth context object for API calls - memoized to prevent infinite loops
76
+ const authContext = useMemo(() => ({
77
+ accessToken,
78
+ refreshToken,
79
+ onError,
80
+ updateTokens,
81
+ }), [accessToken, refreshToken, onError, updateTokens]);
82
+ const [data, setData] = useState<FormData | null>(null);
83
+ const [formData, setFormData] = useState({
84
+ uuid: '',
85
+ case_id: '',
86
+ name: '',
87
+ contact: '',
88
+ requestType: '',
89
+ requestDetails: '',
90
+ supportingDocumentation: '',
91
+ additionalNote: '',
92
+ confirmCheckBox: false,
93
+ });
94
+ const [erasurePurposes, setErasurePurposes] = useState<Purpose[]>([]);
95
+ const [accessPurposes, setAccessPurposes] = useState<Purpose[]>([]);
96
+ const [selectedCorrectionElements, setSelectedCorrectionElements] = useState<
97
+ string[]
98
+ >([]);
99
+ const [uniqueDataElements, setUniqueDataElements] = useState<DataElement[]>(
100
+ []
101
+ );
102
+ const [correctionValues, setCorrectionValues] = useState<
103
+ Record<string, { current: string; updated: string }>
104
+ >({});
105
+ const [caseId, setCaseId] = useState<string | null>(null);
106
+ const fileInputRef = useRef<HTMLInputElement>(null);
107
+ // Store the original English form data for submission — translations are display-only
108
+ const englishDataRef = useRef<FormData | null>(null);
109
+ // Store selections to restore after translation
110
+ const selectionsToRestoreRef = useRef<{
111
+ access: Map<string, { purposeSelected: boolean; dataElementSelections: Map<string, boolean> }>;
112
+ erasure: Map<string, { purposeSelected: boolean; dataElementSelections: Map<string, boolean> }>;
113
+ } | null>(null);
114
+ const [uploadedDocumentId, setUploadedDocumentId] = useState<string[]>([]);
115
+ const [uploadedDocuments, setUploadedDocuments] = useState<
116
+ { uuid: string; name: string }[]
117
+ >([]);
118
+ const [isDocumentUploading, setIsDocumentUploading] = useState(false);
119
+ const [isDocument, setIsDocument] = useState(false);
120
+ const [isPopupFormEnabled, setIsPopupFormEnabled] = useState(!isPopup);
121
+ const [isVerifyingContact, setIsVerifyingContact] = useState(false);
122
+ const requestTypeLabels = useRequestTypeLabels();
123
+ const [grievanceOptions, setGrievanceOptions] = useState<GrievanceOptions[]>(
124
+ []
125
+ );
126
+ const [selectedGrievances, setSelectedGrievances] = useState<
127
+ Record<string, boolean>
128
+ >({});
129
+ const [nominationData, setNominationData] = useState<NominationData>({
130
+ nominee_email: '',
131
+ nominee_mobile: '',
132
+ });
133
+ const [isLoading, setIsLoading] = useState(false);
134
+ const [isTranslating, setIsTranslating] = useState(false);
135
+ const [isRequestDetailsExpanded, setIsRequestDetailsExpanded] =
136
+ useState(true);
137
+ const [isErasureConsentModalOpen, setIsErasureConsentModalOpen] =
138
+ useState(false);
139
+ const [revokeConsentOnFulfilment, setRevokeConsentOnFulfilment] =
140
+ useState(false);
141
+
142
+ const resetFileInput = () => {
143
+ if (fileInputRef.current) {
144
+ fileInputRef.current.value = '';
145
+ }
146
+ };
147
+
148
+ useEffect(() => {
149
+ if (data?.purposes) {
150
+ // Validate purposes array structure
151
+ if (!Array.isArray(data.purposes)) {
152
+ console.warn('Form data purposes is not an array:', data.purposes);
153
+ return;
154
+ }
155
+
156
+ const extractedPurposes = data.purposes
157
+ .map((p) => p?.purpose)
158
+ .filter((p) => p !== null && p !== undefined);
159
+
160
+ if (extractedPurposes.length === 0) {
161
+ console.warn('No valid purposes found in form data');
162
+ return;
163
+ }
164
+
165
+ const uniqueElements = new Map();
166
+ extractedPurposes.forEach((purpose) => {
167
+ // Ensure data_elements is an array and filter out any null/undefined elements
168
+ // Only include data elements with selected === true
169
+ const validDataElements = Array.isArray(purpose.data_elements)
170
+ ? purpose.data_elements.filter(
171
+ (element) => element !== null && element !== undefined && element.uuid && element.selected
172
+ )
173
+ : [];
174
+
175
+ // Data elements are validated and processed
176
+
177
+ validDataElements.forEach((element) => {
178
+ if (element.uuid && !uniqueElements.has(element.uuid)) {
179
+ uniqueElements.set(element.uuid, {
180
+ ...element,
181
+ purposeName: purpose.name,
182
+ });
183
+ }
184
+ });
185
+ });
186
+
187
+ setUniqueDataElements(
188
+ Array.from(uniqueElements.values()) as DataElement[]
189
+ );
190
+
191
+ // Ensure data_elements are properly preserved with all properties
192
+ // Only include data elements with selected === true, and filter out purposes with no data elements
193
+ const processedPurposes = extractedPurposes
194
+ .map((p) => {
195
+ const validDataElements = Array.isArray(p.data_elements)
196
+ ? p.data_elements
197
+ .filter((de) => de !== null && de !== undefined && de.selected)
198
+ .map((de) => ({
199
+ ...de,
200
+ selected: false,
201
+ }))
202
+ : [];
203
+
204
+ return {
205
+ ...p,
206
+ selected: false,
207
+ data_elements: validDataElements,
208
+ };
209
+ })
210
+ .filter((p) => p.data_elements.length > 0);
211
+
212
+ setAccessPurposes(processedPurposes);
213
+ setErasurePurposes(processedPurposes);
214
+ }
215
+ }, [data]);
216
+
217
+ const fetchAndApplyTranslations = useCallback(async (langValue: string) => {
218
+ if (!organizationId || !workspaceId || !data) {
219
+ return;
220
+ }
221
+
222
+ const contact = formData.contact ||
223
+ (typeof window !== 'undefined' ? sessionStorage.getItem('email') || '' : '');
224
+
225
+ if (!contact) {
226
+ return;
227
+ }
228
+
229
+ setIsTranslating(true);
230
+
231
+ try {
232
+ const translatedFormData = await getFormData(
233
+ organizationId,
234
+ workspaceId,
235
+ contact,
236
+ isPopup,
237
+ isCaseManagementContext,
238
+ authContext,
239
+ consentClient,
240
+ langValue
241
+ );
242
+
243
+ if (!translatedFormData) {
244
+ // Fallback to English when translation API unavailable
245
+ toast.info(t('translationNotAvailable'));
246
+ return;
247
+ }
248
+
249
+ const currentAccessSelections = new Map<string, {
250
+ purposeSelected: boolean;
251
+ dataElementSelections: Map<string, boolean>;
252
+ }>();
253
+ const currentErasureSelections = new Map<string, {
254
+ purposeSelected: boolean;
255
+ dataElementSelections: Map<string, boolean>;
256
+ }>();
257
+
258
+ accessPurposes.forEach((purpose) => {
259
+ const dataElementSelections = new Map<string, boolean>();
260
+ purpose.data_elements.forEach((de) => {
261
+ dataElementSelections.set(de.uuid, de.selected);
262
+ });
263
+ currentAccessSelections.set(purpose.uuid, {
264
+ purposeSelected: purpose.selected,
265
+ dataElementSelections,
266
+ });
267
+ });
268
+
269
+ erasurePurposes.forEach((purpose) => {
270
+ const dataElementSelections = new Map<string, boolean>();
271
+ purpose.data_elements.forEach((de) => {
272
+ dataElementSelections.set(de.uuid, de.selected);
273
+ });
274
+ currentErasureSelections.set(purpose.uuid, {
275
+ purposeSelected: purpose.selected,
276
+ dataElementSelections,
277
+ });
278
+ });
279
+
280
+ if (!translatedFormData.purposes ||
281
+ !Array.isArray(translatedFormData.purposes) ||
282
+ translatedFormData.purposes.length === 0) {
283
+ // Fallback to English when no translations for this language
284
+ toast.info(t('translationNotAvailable'));
285
+ return;
286
+ }
287
+
288
+ setData(translatedFormData);
289
+
290
+ if (translatedFormData.grievance_options) {
291
+ const currentGrievanceSelections = { ...selectedGrievances };
292
+ setGrievanceOptions(translatedFormData.grievance_options);
293
+ const updatedGrievanceSelections: Record<string, boolean> = {};
294
+ translatedFormData.grievance_options.forEach((option) => {
295
+ updatedGrievanceSelections[option.value] =
296
+ currentGrievanceSelections[option.value] || false;
297
+ });
298
+ setSelectedGrievances(updatedGrievanceSelections);
299
+ }
300
+
301
+ selectionsToRestoreRef.current = {
302
+ access: currentAccessSelections,
303
+ erasure: currentErasureSelections,
304
+ };
305
+ } catch (error) {
306
+ console.error('Error fetching translation:', error);
307
+ // Fallback to English when translation API fails
308
+ toast.info(t('translationNotAvailable'));
309
+ } finally {
310
+ setIsTranslating(false);
311
+ }
312
+ }, [organizationId, workspaceId, data, formData.contact, isPopup, isCaseManagementContext, authContext, consentClient, accessPurposes, erasurePurposes, selectedGrievances, t]);
313
+
314
+ // Clear previous selections when request type changes
315
+ useEffect(() => {
316
+ setSelectedCorrectionElements([]);
317
+ setCorrectionValues({});
318
+ setNominationData({ nominee_email: '', nominee_mobile: '' });
319
+ if (data?.purposes && Array.isArray(data.purposes)) {
320
+ const extractedPurposes = data.purposes
321
+ .map((p) => p?.purpose)
322
+ .filter((p) => p !== null && p !== undefined);
323
+
324
+ // Ensure data_elements are properly preserved with all properties
325
+ // Only include data elements with selected === true, and filter out purposes with no data elements
326
+ const processedPurposes = extractedPurposes
327
+ .map((p) => {
328
+ const validDataElements = Array.isArray(p.data_elements)
329
+ ? p.data_elements
330
+ .filter((de) => de !== null && de !== undefined && de.selected)
331
+ .map((de) => ({
332
+ ...de,
333
+ selected: false,
334
+ }))
335
+ : [];
336
+
337
+ return {
338
+ ...p,
339
+ selected: false,
340
+ data_elements: validDataElements,
341
+ };
342
+ })
343
+ .filter((p) => p.data_elements.length > 0);
344
+
345
+ setAccessPurposes(processedPurposes);
346
+ setErasurePurposes(processedPurposes);
347
+
348
+ if (
349
+ requestType === RequestTypeEnum.GRIEVANCE &&
350
+ grievanceOptions.length > 0
351
+ ) {
352
+ const resetGrievances = grievanceOptions.reduce(
353
+ (acc, option) => {
354
+ acc[option.value] = false;
355
+ return acc;
356
+ },
357
+ {} as Record<string, boolean>
358
+ );
359
+ setSelectedGrievances(resetGrievances);
360
+ }
361
+ }
362
+ }, [requestType, data, grievanceOptions]);
363
+
364
+
365
+ // Fetch API translations when language changes (purposes, data elements, grievance options)
366
+ useEffect(() => {
367
+ const handleLanguageChanged = () => {
368
+ const lang = i18n.language;
369
+ if (organizationId && workspaceId && data) {
370
+ const contact = formData.contact ||
371
+ (typeof window !== 'undefined' ? sessionStorage.getItem('email') || '' : '');
372
+ if (contact) {
373
+ fetchAndApplyTranslations(lang);
374
+ }
375
+ }
376
+ };
377
+
378
+ i18n.on('languageChanged', handleLanguageChanged);
379
+
380
+ return () => {
381
+ i18n.off('languageChanged', handleLanguageChanged);
382
+ };
383
+ }, [organizationId, workspaceId, data, formData.contact, fetchAndApplyTranslations]);
384
+
385
+ useEffect(() => {
386
+ const initializePrivacyCenterForm = async () => {
387
+ if (isPopup) {
388
+ return;
389
+ }
390
+
391
+ if (!organizationId || !workspaceId) {
392
+ onError?.(new Error('Organization or workspace not found'));
393
+ return;
394
+ }
395
+
396
+ const storedContact =
397
+ typeof window !== 'undefined'
398
+ ? sessionStorage.getItem('email') || ''
399
+ : '';
400
+
401
+ if (!storedContact) {
402
+ onError?.(new Error('Contact email not found'));
403
+ return;
404
+ }
405
+
406
+ try {
407
+ // Always fetch English data first and store for submission
408
+ const englishData = await getFormData(
409
+ organizationId,
410
+ workspaceId,
411
+ storedContact,
412
+ isPopup,
413
+ isCaseManagementContext,
414
+ authContext,
415
+ consentClient
416
+ );
417
+
418
+ if (!englishData) {
419
+ toast.error(t('failedToFetchFormData'));
420
+ onError?.(new Error('Failed to fetch form data'));
421
+ return;
422
+ }
423
+
424
+ englishDataRef.current = englishData;
425
+
426
+ // If current language is non-English, fetch translated data for display
427
+ const langParam = getLangParam(i18n.language);
428
+ const displayData = langParam
429
+ ? (await getFormData(
430
+ organizationId,
431
+ workspaceId,
432
+ storedContact,
433
+ isPopup,
434
+ isCaseManagementContext,
435
+ authContext,
436
+ consentClient,
437
+ langParam
438
+ )) || englishData
439
+ : englishData;
440
+
441
+ if (displayData.purposes && Array.isArray(displayData.purposes)) {
442
+ const purposesWithDataElements = displayData.purposes.filter(
443
+ (p) => p.purpose?.data_elements && p.purpose.data_elements.length > 0
444
+ );
445
+
446
+ if (purposesWithDataElements.length === 0) {
447
+ console.warn('No purposes with data elements found in API response');
448
+ }
449
+ }
450
+
451
+ setData(displayData);
452
+ setGrievanceOptions(displayData.grievance_options || []);
453
+ const initialGrievances = (
454
+ displayData.grievance_options || []
455
+ ).reduce(
456
+ (acc, option) => {
457
+ acc[option.value] = false;
458
+ return acc;
459
+ },
460
+ {} as Record<string, boolean>
461
+ );
462
+ setSelectedGrievances(initialGrievances);
463
+ setFormData((prev) => ({
464
+ ...prev,
465
+ uuid: displayData.uuid || '',
466
+ name: displayData.name || '',
467
+ contact: displayData.contact || storedContact,
468
+ }));
469
+ } finally {
470
+ }
471
+ };
472
+
473
+ void initializePrivacyCenterForm();
474
+ }, [organizationId, workspaceId, isPopup, authContext, consentClient]);
475
+
476
+ /**
477
+ * Maps translated correction element names back to English names using UUID lookup.
478
+ */
479
+ const getEnglishDataElementName = useCallback((translatedName: string): string => {
480
+ if (!englishDataRef.current?.purposes){
481
+ return translatedName
482
+ }
483
+
484
+ // Build a map from translated name -> english name via UUID
485
+ const translatedToUuid = new Map<string, string>();
486
+ if (data?.purposes) {
487
+ data.purposes.forEach((pw) => {
488
+ pw.purpose?.data_elements?.forEach((de) => {
489
+ translatedToUuid.set(de.name, de.uuid);
490
+ });
491
+ });
492
+ }
493
+
494
+ const uuid = translatedToUuid.get(translatedName);
495
+ if (!uuid){
496
+ return translatedName
497
+ }
498
+
499
+ for (const pw of englishDataRef.current.purposes) {
500
+ const englishDe = pw.purpose?.data_elements?.find((de) => de.uuid === uuid);
501
+ if (englishDe){
502
+ return englishDe.name
503
+ }
504
+ }
505
+ return translatedName;
506
+ }, [data]);
507
+
508
+ const selectedErasurePurposeList = useMemo(
509
+ () =>
510
+ erasurePurposes
511
+ .filter((p) => p.selected || p.data_elements.some((de) => de.selected))
512
+ .map((p) => p.name)
513
+ .filter(Boolean),
514
+ [erasurePurposes]
515
+ );
516
+
517
+ const selectedErasurePurposeNames = useMemo(() => {
518
+ if (
519
+ selectedErasurePurposeList.length === 0 ||
520
+ selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL
521
+ ) {
522
+ return t('revokeConsentModalYourPurposes');
523
+ }
524
+ return selectedErasurePurposeList.join(', ');
525
+ }, [selectedErasurePurposeList, t]);
526
+
527
+ const handleSubmitClick = () => {
528
+ if (!isFormValid){
529
+ return
530
+ };
531
+ if (requestType === RequestTypeEnum.ERASURE) {
532
+ setIsErasureConsentModalOpen(true);
533
+ return;
534
+ }
535
+ setRevokeConsentOnFulfilment(false);
536
+ void handleSubmit(false);
537
+ };
538
+
539
+ const handleErasureModalChoice = (revoke: boolean) => {
540
+ setRevokeConsentOnFulfilment(revoke);
541
+ setIsErasureConsentModalOpen(false);
542
+ void handleSubmit(revoke);
543
+ };
544
+
545
+ const handleErasureModalClose = () => {
546
+ setIsErasureConsentModalOpen(false);
547
+ };
548
+
549
+ const handleSubmit = async (revokeConsent: boolean = false) => {
550
+ if (!isFormValid){
551
+ return
552
+ };
553
+
554
+ const requestDetails: {
555
+ purposes: UpdatePurpose[];
556
+ correction_data: UpdateCorrectionDataItem[];
557
+ grievance_types: GrievanceTypeElement[];
558
+ nomination_data?: NominationData;
559
+ } = {
560
+ purposes: [],
561
+ correction_data: [],
562
+ grievance_types: [],
563
+ };
564
+
565
+ if (requestType === RequestTypeEnum.ACCESS) {
566
+ requestDetails.purposes = accessPurposes
567
+ .filter((p) => p.selected || p.data_elements.some((de) => de.selected))
568
+ .map((p) => ({
569
+ purpose_uuid: p.uuid,
570
+ data_element_uuids: p.data_elements
571
+ .filter((de) => de.selected)
572
+ .map((de) => de.uuid),
573
+ }));
574
+ }
575
+
576
+ if (requestType === RequestTypeEnum.ERASURE) {
577
+ requestDetails.purposes = erasurePurposes
578
+ .filter((p) => p.selected || p.data_elements.some((de) => de.selected))
579
+ .map((p) => ({
580
+ purpose_uuid: p.uuid,
581
+ data_element_uuids: p.data_elements
582
+ .filter((de) => de.selected)
583
+ .map((de) => de.uuid),
584
+ }));
585
+ }
586
+
587
+ if (requestType === RequestTypeEnum.CORRECTION) {
588
+ requestDetails.correction_data = selectedCorrectionElements.map(
589
+ (name) => ({
590
+ name: getEnglishDataElementName(name),
591
+ curr_value: correctionValues[name]?.current || '',
592
+ new_value: correctionValues[name]?.updated || '',
593
+ })
594
+ );
595
+ }
596
+
597
+ if (requestType === RequestTypeEnum.GRIEVANCE) {
598
+ requestDetails.grievance_types = Object.entries(selectedGrievances)
599
+ .filter(([, checked]) => checked)
600
+ .map(([key]) => ({
601
+ grievance: key as GrievanceValue,
602
+ }));
603
+ }
604
+
605
+ if (requestType === RequestTypeEnum.NOMINATION) {
606
+ requestDetails.nomination_data = {
607
+ nominee_email: nominationData.nominee_email.trim(),
608
+ ...(nominationData.nominee_mobile?.trim()
609
+ ? { nominee_mobile: nominationData.nominee_mobile.trim() }
610
+ : {}),
611
+ };
612
+ }
613
+
614
+ const payload: UserDataRequest = {
615
+ name: ' ',
616
+ contact: formData.contact,
617
+ request_type: requestType?.toLowerCase() as RequestTypeEnum,
618
+ uuid: formData.uuid,
619
+ supporting_docs_uuids: [...uploadedDocumentId],
620
+ request_details: requestDetails,
621
+ time_period:
622
+ requestType === RequestTypeEnum.ACCESS
623
+ ? parseInt(timePeriod || '30', 10)
624
+ : 0,
625
+ requestor_note: formData.additionalNote || '',
626
+ ...(requestType === RequestTypeEnum.ERASURE
627
+ ? { revoke_consent_on_fulfilment: revokeConsent }
628
+ : {}),
629
+ };
630
+
631
+ try {
632
+ setIsLoading(true);
633
+ const response = await createCaseRequest(
634
+ organizationId || '',
635
+ workspaceId || '',
636
+ payload,
637
+ isPopup,
638
+ isCaseManagementContext, // Use session auth if in case management context
639
+ authContext,
640
+ consentClient
641
+ );
642
+ // privacyCenterApiFetch unwraps { code, status, detail } responses and returns just the detail
643
+ // So response will be { uuid, case_id } if successful (privacy center), or null if error
644
+ // Case management returns the full CreateCaseResponse with { code, status, detail: { uuid, case_id } }
645
+ let caseId: string | null = null;
646
+ if (response) {
647
+ if ('detail' in response && response.detail) {
648
+ // Case management: wrapped response
649
+ caseId =
650
+ (response.detail as { case_id?: string; uuid?: string }).case_id ||
651
+ (response.detail as { case_id?: string; uuid?: string }).uuid ||
652
+ null;
653
+ } else if ('case_id' in response || 'uuid' in response) {
654
+ // Privacy center: unwrapped response
655
+ caseId =
656
+ (response as { case_id?: string; uuid?: string }).case_id ||
657
+ (response as { case_id?: string; uuid?: string }).uuid ||
658
+ null;
659
+ }
660
+ }
661
+
662
+ if (caseId) {
663
+ queryClient.invalidateQueries({ queryKey: ['caseHistory'] });
664
+ setCaseId(caseId);
665
+ setIsSubmitted(true);
666
+ toast.success(t('requestSubmittedSuccess'));
667
+
668
+ if (isPopup) {
669
+ onCreateRequest?.();
670
+ }
671
+ } else {
672
+ toast.error(t('requestSubmissionFailed'));
673
+ }
674
+ } catch (error) {
675
+ console.error('Error submitting request:', error);
676
+ toast.error(t('submissionFailed'));
677
+ } finally {
678
+ setIsLoading(false);
679
+ if (isPopup) {
680
+ onClose?.();
681
+ }
682
+ }
683
+ };
684
+
685
+ const isRequestDetailsValid = useCallback(() => {
686
+ if (!requestType){
687
+ return false
688
+ };
689
+ switch (requestType) {
690
+ case RequestTypeEnum.ACCESS:
691
+ return accessPurposes.some(
692
+ (p) => p.selected || p.data_elements.some((de) => de.selected)
693
+ );
694
+
695
+ case RequestTypeEnum.ERASURE:
696
+ return erasurePurposes.some(
697
+ (p) => p.selected || p.data_elements.some((de) => de.selected)
698
+ );
699
+
700
+ case RequestTypeEnum.GRIEVANCE:
701
+ return Object.values(selectedGrievances).some(Boolean);
702
+
703
+ case RequestTypeEnum.CORRECTION:
704
+ return (
705
+ selectedCorrectionElements.length > 0 &&
706
+ selectedCorrectionElements.every(
707
+ (name) =>
708
+ correctionValues[name]?.current?.trim() &&
709
+ correctionValues[name]?.updated?.trim()
710
+ )
711
+ );
712
+
713
+ case RequestTypeEnum.NOMINATION:
714
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(nominationData.nominee_email.trim());
715
+
716
+ default:
717
+ return false;
718
+ }
719
+ }, [
720
+ requestType,
721
+ accessPurposes,
722
+ erasurePurposes,
723
+ selectedGrievances,
724
+ selectedCorrectionElements,
725
+ correctionValues,
726
+ nominationData,
727
+ ]);
728
+
729
+ const isFormValid = useMemo(() => {
730
+ return (
731
+ (!isPopup || isPopupFormEnabled) &&
732
+ !!formData.contact?.trim() &&
733
+ !!requestType &&
734
+ isRequestDetailsValid() &&
735
+ formData.confirmCheckBox
736
+ );
737
+ }, [
738
+ isPopup,
739
+ isPopupFormEnabled,
740
+ formData.contact,
741
+ formData.confirmCheckBox,
742
+ requestType,
743
+ isRequestDetailsValid,
744
+ ]);
745
+
746
+ const handleFileUpload = async (
747
+ file: File,
748
+ isPublic: boolean,
749
+ caseId: string
750
+ ) => {
751
+ setIsDocumentUploading(true);
752
+ try {
753
+ const metaData = {
754
+ group_by: 'privacy',
755
+ case_uuid: caseId,
756
+ product: 'privacy_center',
757
+ };
758
+
759
+ const payload = {
760
+ organization_uuid: organizationId || '',
761
+ space: `privacy/${caseId}`,
762
+ uuid: crypto.randomUUID(),
763
+ is_public: isPublic,
764
+ allow_ai_processing: true,
765
+ ...(metaData ? { metadata: metaData } : {}),
766
+ };
767
+
768
+ // Create FormData on client side - this can be passed to server actions
769
+ const formData = new FormData();
770
+ formData.append('file', file);
771
+ formData.append('payload', JSON.stringify(payload));
772
+
773
+ // Ensure we have a valid token before uploading
774
+ if (!organizationId || !workspaceId) {
775
+ throw new Error('Organization ID and Workspace ID are required for file upload');
776
+ }
777
+
778
+ const authToken = await ensureValidToken(organizationId, workspaceId, orgSlug, authContext, consentClient);
779
+ if (!authToken) {
780
+ throw new Error('Authentication token is required for file upload');
781
+ }
782
+
783
+ // uploadFileWithS2S is a client-side function that uses JWT Bearer token authentication
784
+ const uploadedDoc = await uploadFileWithS2S({
785
+ formData,
786
+ fileName: file.name,
787
+ authToken,
788
+ baseUrl,
789
+ });
790
+
791
+ if (!uploadedDoc.success) {
792
+ toast.error(t('failedToUploadFile', { fileName: file.name }));
793
+ throw new Error(uploadedDoc.error);
794
+ }
795
+
796
+ if (uploadedDoc.uuid) {
797
+ setUploadedDocuments((prev) => [
798
+ ...prev,
799
+ { uuid: uploadedDoc.uuid!, name: file.name },
800
+ ]);
801
+ setUploadedDocumentId((prevIds) => [...prevIds, uploadedDoc.uuid!]);
802
+ setIsDocument(true);
803
+ toast.success(t('fileUploadedSuccess'));
804
+ }
805
+ } catch (error) {
806
+ console.error('Upload error:', error);
807
+ toast.error(t('uploadError', { error: String(error) }));
808
+ return null;
809
+ } finally {
810
+ setIsDocumentUploading(false);
811
+ }
812
+ };
813
+
814
+ const handleFileChange = async (
815
+ event: React.ChangeEvent<HTMLInputElement>
816
+ ) => {
817
+ const files = Array.from(event.target.files || []);
818
+ handleFileUpload(files[0], true, formData.uuid);
819
+ resetFileInput();
820
+ };
821
+
822
+ const handleRemoveDocument = (uuidToRemove: string) => {
823
+ setUploadedDocuments((prev) =>
824
+ prev.filter((doc) => doc.uuid !== uuidToRemove)
825
+ );
826
+ setUploadedDocumentId((prev) => prev.filter((id) => id !== uuidToRemove));
827
+ };
828
+
829
+ const handleVerifyContact = async () => {
830
+ if (!isPopup) {
831
+ return
832
+ };
833
+ if (!formData.contact) {
834
+ toast.error(t('pleaseEnterContactToVerify'));
835
+ return;
836
+ }
837
+ if (!organizationId || !workspaceId) {
838
+ toast.error(t('missingOrgOrWorkspace'));
839
+ return;
840
+ }
841
+
842
+ setIsVerifyingContact(true);
843
+ try {
844
+ // Always fetch English data first and store for submission
845
+ const englishData = await getFormData(
846
+ organizationId,
847
+ workspaceId,
848
+ formData.contact,
849
+ isPopup,
850
+ isCaseManagementContext,
851
+ authContext,
852
+ consentClient
853
+ );
854
+
855
+ if (englishData) {
856
+ englishDataRef.current = englishData;
857
+
858
+ // If current language is non-English, fetch translated data for display
859
+ const langParam = getLangParam(i18n.language);
860
+ const displayData = langParam
861
+ ? (await getFormData(
862
+ organizationId,
863
+ workspaceId,
864
+ formData.contact,
865
+ isPopup,
866
+ isCaseManagementContext,
867
+ authContext,
868
+ consentClient,
869
+ langParam
870
+ )) || englishData
871
+ : englishData;
872
+
873
+ if (displayData.purposes && Array.isArray(displayData.purposes)) {
874
+ const purposesWithDataElements = displayData.purposes.filter(
875
+ (p) => p.purpose?.data_elements && p.purpose.data_elements.length > 0
876
+ );
877
+
878
+ if (purposesWithDataElements.length === 0) {
879
+ console.warn('No purposes with data elements found in API response (popup)');
880
+ }
881
+ }
882
+
883
+ setData(displayData);
884
+ setGrievanceOptions(displayData.grievance_options || []);
885
+ const initialGrievances = (displayData.grievance_options || []).reduce(
886
+ (acc, option) => {
887
+ acc[option.value] = false;
888
+ return acc;
889
+ },
890
+ {} as Record<string, boolean>
891
+ );
892
+ setSelectedGrievances(initialGrievances);
893
+ setFormData((prev) => ({
894
+ ...prev,
895
+ uuid: displayData.uuid || '',
896
+ name: displayData.name || '',
897
+ contact: displayData.contact || prev.contact,
898
+ }));
899
+ // Translations are handled locally via i18next, no API call needed
900
+ setIsPopupFormEnabled(true);
901
+ toast.success(t('contactVerifiedSuccess'));
902
+ } else {
903
+ setIsPopupFormEnabled(false);
904
+ toast.error(t('unableToVerifyContact'));
905
+ }
906
+ } catch (error: any) {
907
+ if (error.response?.status === 404) {
908
+ toast.info(t('noUserFoundForContact', { contact: formData.contact }));
909
+ onClose?.();
910
+ } else {
911
+ toast.error(t('failedToVerifyContact'));
912
+ setIsVerifyingContact(false);
913
+ }
914
+ } finally {
915
+ setIsVerifyingContact(false);
916
+ }
917
+ };
918
+
919
+ return (
920
+ <>
921
+ {isSubmitted && !isPopup ? (
922
+ <RequestSubmit
923
+ caseId={caseId || ''}
924
+ contact={formData?.contact}
925
+ onCreateRequest={onCreateRequest}
926
+ revokeConsentOnFulfilment={revokeConsentOnFulfilment}
927
+ revokedPurposeNames={selectedErasurePurposeNames}
928
+ />
929
+ ) : (
930
+ <div>
931
+ <div>
932
+ {!isPopup && (
933
+ <p style={{
934
+ marginTop: 0,
935
+ marginBottom: getVar('--pc-spacing-sm'),
936
+ lineHeight: 1.5,
937
+ fontSize: pcStyles.textSm.fontSize,
938
+ color: getVar('--pc-text-secondary'),
939
+ }}>{t('description')}</p>
940
+ )}
941
+ <div
942
+ style={{
943
+ backgroundColor: getVar('--pc-bg-primary'),
944
+ ...(isPopup ? {
945
+ marginTop: 0,
946
+ marginBottom: 0,
947
+ borderRadius: pcStyles.borderRadiusXl,
948
+ } : {
949
+ marginTop: pcStyles.mtSm.marginTop,
950
+ marginBottom: pcStyles.mbSm.marginBottom,
951
+ borderRadius: pcStyles.borderRadiusXl,
952
+ border: `1px solid ${getVar('--pc-border-color')}`,
953
+ boxShadow: getVar('--pc-shadow-sm'),
954
+ }),
955
+ }}
956
+ >
957
+ <FormBlockWrapper inputClassName="mx-auto">
958
+ <div
959
+ style={{
960
+ ...pcStyles.flex,
961
+ ...pcStyles.itemsCenter,
962
+ ...pcStyles.justifyBetween,
963
+ backgroundColor: `var(--pc-bg-secondary)`,
964
+ padding: getVar('--pc-spacing-md'),
965
+ width: '100%',
966
+ overflow: 'visible',
967
+ ...(isPopup ? {
968
+ position: 'sticky',
969
+ top: 0,
970
+ zIndex: 20,
971
+ borderBottom: `1px solid var(--pc-border-color)`,
972
+ } : {
973
+ position: 'relative',
974
+ borderRadius: `${getVar('--pc-border-radius-lg')} ${getVar('--pc-border-radius-lg')} 0 0`,
975
+ }),
976
+ }}
977
+ className="pc-padding-responsive"
978
+ >
979
+ <p style={{
980
+ fontSize: pcStyles.textSm.fontSize,
981
+ fontWeight: pcStyles.fontWeightSemibold.fontWeight,
982
+ color: `var(--pc-text-primary)`,
983
+ margin: 0,
984
+ whiteSpace: 'nowrap',
985
+ flexShrink: 0,
986
+ flexGrow: 0,
987
+ }} className="pc-text-responsive">
988
+ {t('raiseDataRequest')}
989
+ </p>
990
+ {isPopup && (
991
+ <PCButton
992
+ variant="text"
993
+ onClick={onClose}
994
+ style={{ padding: 0, flexShrink: 0, marginLeft: 'auto' }}
995
+ >
996
+ <FiX size={20} />
997
+ </PCButton>
998
+ )}
999
+ </div>
1000
+ <div
1001
+ style={{
1002
+ ...pcStyles.flex,
1003
+ ...pcStyles.flexCol,
1004
+ gap: getVar('--pc-spacing-lg'),
1005
+ padding: getVar('--pc-spacing-lg'),
1006
+ }}
1007
+ className="pc-padding-responsive pc-spacing-responsive"
1008
+ >
1009
+ <div style={{
1010
+ ...pcStyles.flex,
1011
+ width: '100%',
1012
+ ...pcStyles.itemsEnd,
1013
+ gap: getVar('--pc-spacing-sm'),
1014
+ flexWrap: 'wrap',
1015
+ }}>
1016
+ <PCInput
1017
+ style={{ flex: 1, minWidth: '200px' }}
1018
+ label={
1019
+ <span>
1020
+ {t('contact')}
1021
+ <span style={{ color: getVar('--pc-error') }}>*</span>
1022
+ </span>
1023
+ }
1024
+ placeholder={t('contactPlaceholder')}
1025
+ value={formData.contact}
1026
+ readOnly={!isPopup}
1027
+ onChange={(e) => {
1028
+ if (!isPopup){
1029
+ return
1030
+ };
1031
+ setFormData({
1032
+ ...formData,
1033
+ contact: e.target.value,
1034
+ });
1035
+ setIsPopupFormEnabled(false);
1036
+ }}
1037
+ />
1038
+ {isPopup && !isPopupFormEnabled && (
1039
+ <div style={{ ...pcStyles.flex, ...pcStyles.justifyEnd }}>
1040
+ <PCButton
1041
+ size="sm"
1042
+ onClick={handleVerifyContact}
1043
+ style={{ height: '2.25rem', flexShrink: 0 }}
1044
+ disabled={!formData.contact || isVerifyingContact}
1045
+ >
1046
+ {isVerifyingContact ? t('verifying') : t('verify')}
1047
+ </PCButton>
1048
+ </div>
1049
+ )}
1050
+ </div>
1051
+ <PCSelect
1052
+ label={
1053
+ <span>
1054
+ {t('requestType')}
1055
+ <span style={{ color: getVar('--pc-error') }}>*</span>
1056
+ </span>
1057
+ }
1058
+ options={Object.values(RequestTypeEnum).map((type) => ({
1059
+ value: type,
1060
+ label: requestTypeLabels[type],
1061
+ }))}
1062
+ value={
1063
+ Object.values(RequestTypeEnum)
1064
+ .map((type) => ({
1065
+ value: type,
1066
+ label: requestTypeLabels[type],
1067
+ }))
1068
+ .find((opt) => opt.value === requestType) || null
1069
+ }
1070
+ onChange={(value) => {
1071
+ if (value) {
1072
+ setRequestType(value.value as RequestTypeEnum);
1073
+ setTimePeriod('');
1074
+ setIsRequestDetailsExpanded(true);
1075
+ }
1076
+ }}
1077
+ placeholder={t('selectRequestType')}
1078
+ disabled={isPopup && !isPopupFormEnabled}
1079
+ />
1080
+ <div
1081
+ style={{
1082
+ ...pcStyles.flex,
1083
+ ...pcStyles.flexCol,
1084
+ borderRadius: pcStyles.borderRadiusLg,
1085
+ border: `1px solid ${getVar('--pc-border-color')}`,
1086
+ backgroundColor: getVar('--pc-bg-secondary'),
1087
+ overflow: 'hidden',
1088
+ ...(isPopup && !isPopupFormEnabled ? {
1089
+ pointerEvents: 'none',
1090
+ opacity: 0.6,
1091
+ } : {}),
1092
+ }}
1093
+ >
1094
+ <div style={{
1095
+ ...pcStyles.flex,
1096
+ ...pcStyles.itemsCenter,
1097
+ ...pcStyles.justifyBetween,
1098
+ padding: getVar('--pc-spacing-md'),
1099
+ borderBottom: isRequestDetailsExpanded ? `1px solid ${getVar('--pc-border-color')}` : 'none',
1100
+ }}>
1101
+ <p style={{
1102
+ fontWeight: pcStyles.fontWeightSemibold.fontWeight,
1103
+ color: `var(--pc-text-primary)`,
1104
+ fontSize: pcStyles.textSm.fontSize,
1105
+ margin: 0,
1106
+ }}>
1107
+ {t('requestDetails')}{' '}
1108
+ <span style={{ color: getVar('--pc-error') }}>*</span>
1109
+ </p>
1110
+ <PCButton
1111
+ variant="text"
1112
+ size="sm"
1113
+ style={{ height: 'auto', padding: 0, color: 'var(--pc-text-secondary)', flexShrink: 0 }}
1114
+ onClick={() =>
1115
+ setIsRequestDetailsExpanded(
1116
+ !isRequestDetailsExpanded
1117
+ )
1118
+ }
1119
+ >
1120
+ {isRequestDetailsExpanded ? (
1121
+ <FiChevronUp size={20} />
1122
+ ) : (
1123
+ <FiChevronDown size={20} />
1124
+ )}
1125
+ </PCButton>
1126
+ </div>
1127
+ {isRequestDetailsExpanded && (
1128
+ <div style={{
1129
+ padding: getVar('--pc-spacing-md'),
1130
+ }}>
1131
+ {requestType === RequestTypeEnum.ACCESS ||
1132
+ requestType === RequestTypeEnum.ERASURE ? (
1133
+ <PurposeSelectionSection
1134
+ purposes={
1135
+ requestType === RequestTypeEnum.ACCESS
1136
+ ? accessPurposes
1137
+ : erasurePurposes
1138
+ }
1139
+ setPurposes={
1140
+ requestType === RequestTypeEnum.ACCESS
1141
+ ? setAccessPurposes
1142
+ : setErasurePurposes
1143
+ }
1144
+ requestType={requestType}
1145
+ isTranslating={isTranslating}
1146
+ />
1147
+ ) : requestType === RequestTypeEnum.CORRECTION ? (
1148
+ <CorrectionDataSection
1149
+ dataElements={uniqueDataElements}
1150
+ selected={selectedCorrectionElements}
1151
+ setSelected={setSelectedCorrectionElements}
1152
+ correctionValues={correctionValues}
1153
+ setCorrectionValues={setCorrectionValues}
1154
+ isTranslating={isTranslating}
1155
+ />
1156
+ ) : requestType === RequestTypeEnum.GRIEVANCE ? (
1157
+ <GrievanceDataSection
1158
+ grievanceOptions={grievanceOptions}
1159
+ selectedGrievances={selectedGrievances}
1160
+ setSelectedGrievances={setSelectedGrievances}
1161
+ isTranslating={isTranslating}
1162
+ />
1163
+ ) : requestType === RequestTypeEnum.NOMINATION ? (
1164
+ <NominationDataSection
1165
+ nominationData={nominationData}
1166
+ setNominationData={setNominationData}
1167
+ isTranslating={isTranslating}
1168
+ />
1169
+ ) : (
1170
+ <div
1171
+ style={{
1172
+ color: getVar('--pc-text-secondary'),
1173
+ ...(isPopup ? {
1174
+ padding: getVar('--pc-spacing-md'),
1175
+ } : {
1176
+ ...pcStyles.flex,
1177
+ height: '8rem',
1178
+ ...pcStyles.itemsCenter,
1179
+ ...pcStyles.justifyCenter,
1180
+ padding: getVar('--pc-spacing-lg'),
1181
+ }),
1182
+ textAlign: 'center',
1183
+ }}
1184
+ >
1185
+ {t('selectRequestType')}
1186
+ </div>
1187
+ )}
1188
+ </div>
1189
+ )}
1190
+ </div>
1191
+ <div
1192
+ style={{
1193
+ ...pcStyles.flex,
1194
+ ...pcStyles.flexCol,
1195
+ gap: getVar('--pc-spacing-sm'),
1196
+ ...(isPopup && !isPopupFormEnabled ? {
1197
+ pointerEvents: 'none',
1198
+ opacity: 0.6,
1199
+ } : {}),
1200
+ }}
1201
+ >
1202
+ <p style={{
1203
+ fontWeight: pcStyles.fontWeightSemibold.fontWeight,
1204
+ color: `var(--pc-text-primary)`,
1205
+ fontSize: pcStyles.textSm.fontSize,
1206
+ margin: 0,
1207
+ }}>
1208
+ {t('supportingDocumentation')}
1209
+ </p>
1210
+ <input
1211
+ ref={fileInputRef}
1212
+ id="privacy-file-upload"
1213
+ type="file"
1214
+ onChange={(e) => handleFileChange(e)}
1215
+ style={{ display: 'none' }}
1216
+ />
1217
+ <div
1218
+ style={{
1219
+ ...pcStyles.flex,
1220
+ flexWrap: 'wrap',
1221
+ ...pcStyles.itemsStart,
1222
+ borderRadius: pcStyles.borderRadiusMd,
1223
+ border: `1px solid var(--pc-border-color)`,
1224
+ backgroundColor: `var(--pc-bg-secondary)`,
1225
+ padding: getVar('--pc-spacing-md'),
1226
+ minHeight: '100px',
1227
+ ...(isPopup ? {
1228
+ gap: '0.5rem',
1229
+ ...pcStyles.overflowYAuto,
1230
+ } : {
1231
+ gap: getVar('--pc-spacing-sm'),
1232
+ }),
1233
+ }}
1234
+ >
1235
+ {/* Always render documents list container to prevent layout shift */}
1236
+ {(isDocument && uploadedDocuments.length > 0) && (
1237
+ <div
1238
+ style={{
1239
+ ...pcStyles.flex,
1240
+ ...pcStyles.gapSm,
1241
+ ...(isPopup ? { flexWrap: 'wrap' } : {}),
1242
+ width: '100%',
1243
+ }}
1244
+ >
1245
+ {uploadedDocuments.map((doc) => (
1246
+ <div
1247
+ key={doc.uuid}
1248
+ style={{
1249
+ position: 'relative',
1250
+ ...pcStyles.flex,
1251
+ ...pcStyles.itemsCenter,
1252
+ ...pcStyles.gapXs,
1253
+ borderRadius: pcStyles.borderRadiusMd,
1254
+ border: `1px solid var(--pc-border-color)`,
1255
+ backgroundColor: `var(--pc-bg-primary)`,
1256
+ padding: getVar('--pc-spacing-sm'),
1257
+ }}
1258
+ >
1259
+ <img
1260
+ src={fileAttachIcon}
1261
+ alt="fileAttach"
1262
+ width={24}
1263
+ height={24}
1264
+ />
1265
+ <p
1266
+ style={{
1267
+ fontWeight: pcStyles.fontWeightMedium.fontWeight,
1268
+ color: `var(--pc-text-primary)`,
1269
+ ...(isPopup ? {
1270
+ maxWidth: '100px',
1271
+ overflow: 'hidden',
1272
+ textOverflow: 'ellipsis',
1273
+ whiteSpace: 'nowrap',
1274
+ fontSize: pcStyles.textXs.fontSize,
1275
+ } : {
1276
+ fontSize: pcStyles.textSm.fontSize,
1277
+ }),
1278
+ }}
1279
+ >
1280
+ {doc.name}
1281
+ </p>
1282
+ <div
1283
+ style={{
1284
+ position: 'absolute',
1285
+ right: '-0.25rem',
1286
+ top: '-0.5rem',
1287
+ cursor: 'pointer',
1288
+ }}
1289
+ onClick={(e) => {
1290
+ e.stopPropagation();
1291
+ handleRemoveDocument(doc.uuid);
1292
+ }}
1293
+ >
1294
+ <div style={{
1295
+ borderRadius: '50%',
1296
+ border: `1px solid var(--pc-border-color)`,
1297
+ backgroundColor: `var(--pc-bg-secondary)`,
1298
+ padding: '0.125rem',
1299
+ }}>
1300
+ <AiOutlineClose size={12} style={{ color: 'var(--pc-error)' }} />
1301
+ </div>
1302
+ </div>
1303
+ </div>
1304
+ ))}
1305
+ </div>
1306
+ )}
1307
+
1308
+ {/* Upload button or loading state - always in same position */}
1309
+ <div style={{
1310
+ width: uploadedDocuments.length > 0 ? 'auto' : '100%',
1311
+ ...pcStyles.flex,
1312
+ ...pcStyles.itemsCenter,
1313
+ ...pcStyles.justifyCenter,
1314
+ minHeight: uploadedDocuments.length > 0 ? 'auto' : '100px',
1315
+ }}>
1316
+ {isDocumentUploading ? (
1317
+ <div style={{
1318
+ padding: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-lg'),
1319
+ borderRadius: pcStyles.borderRadiusMd,
1320
+ border: uploadedDocuments.length > 0 ? `1px solid var(--pc-border-color)` : 'none',
1321
+ backgroundColor: `var(--pc-bg-primary)`,
1322
+ fontWeight: pcStyles.fontWeightSemibold.fontWeight,
1323
+ color: `var(--pc-text-primary)`,
1324
+ ...pcStyles.flex,
1325
+ ...pcStyles.itemsCenter,
1326
+ ...pcStyles.justifyCenter,
1327
+ gap: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-md'),
1328
+ width: 'auto',
1329
+ }}>
1330
+ <PCLoader variant="spinner" size="sm" />
1331
+ <span>{t('uploading')}</span>
1332
+ </div>
1333
+ ) : (
1334
+ <div
1335
+ style={{
1336
+ ...pcStyles.flex,
1337
+ width: uploadedDocuments.length > 0 ? 'auto' : '100%',
1338
+ cursor: 'pointer',
1339
+ ...pcStyles.itemsCenter,
1340
+ ...pcStyles.justifyCenter,
1341
+ gap: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-md'),
1342
+ borderRadius: pcStyles.borderRadiusMd,
1343
+ padding: uploadedDocuments.length > 0 ? getVar('--pc-spacing-sm') : getVar('--pc-spacing-lg'),
1344
+ border: uploadedDocuments.length > 0 ? `1px solid var(--pc-border-color)` : 'none',
1345
+ backgroundColor: uploadedDocuments.length > 0 ? `var(--pc-bg-primary)` : 'transparent',
1346
+ transition: 'all 0.2s ease',
1347
+ }}
1348
+ onMouseEnter={(e) => {
1349
+ e.currentTarget.style.backgroundColor = uploadedDocuments.length > 0
1350
+ ? `var(--pc-bg-hover)`
1351
+ : `var(--pc-bg-hover)`;
1352
+ if (uploadedDocuments.length > 0) {
1353
+ e.currentTarget.style.borderColor = `var(--pc-primary)`;
1354
+ }
1355
+ }}
1356
+ onMouseLeave={(e) => {
1357
+ e.currentTarget.style.backgroundColor = uploadedDocuments.length > 0
1358
+ ? `var(--pc-bg-primary)`
1359
+ : 'transparent';
1360
+ if (uploadedDocuments.length > 0) {
1361
+ e.currentTarget.style.borderColor = `var(--pc-border-color)`;
1362
+ }
1363
+ }}
1364
+ role="button"
1365
+ tabIndex={0}
1366
+ aria-label={t('uploadFile')}
1367
+ onClick={() =>
1368
+ document
1369
+ .getElementById('privacy-file-upload')
1370
+ ?.click()
1371
+ }
1372
+ onKeyDown={(e) => {
1373
+ if (e.key === 'Enter' || e.key === ' ') {
1374
+ e.preventDefault();
1375
+ document.getElementById('privacy-file-upload')?.click();
1376
+ }
1377
+ }}
1378
+ >
1379
+ {uploadedDocuments.length === 0 ? (
1380
+ <>
1381
+ <div>
1382
+ <img
1383
+ src={uploadFileIcon}
1384
+ width={44}
1385
+ height={44}
1386
+ alt="upload"
1387
+ />
1388
+ </div>
1389
+ <span style={{
1390
+ color: 'var(--pc-text-secondary)',
1391
+ fontSize: pcStyles.textSm.fontSize,
1392
+ }}>
1393
+ {t('uploadFile')}
1394
+ </span>
1395
+ </>
1396
+ ) : (
1397
+ <div style={{ ...pcStyles.flex, ...pcStyles.itemsCenter, ...pcStyles.justifyCenter, ...pcStyles.gapSm }}>
1398
+ <PiUploadBold
1399
+ size={isPopup ? 20 : 16}
1400
+ style={{ color: 'var(--pc-text-secondary)' }}
1401
+ />
1402
+ {!isPopup && (
1403
+ <p style={{
1404
+ fontWeight: pcStyles.fontWeightMedium.fontWeight,
1405
+ color: `var(--pc-text-primary)`,
1406
+ }}>
1407
+ {t('uploadFileButton')}
1408
+ </p>
1409
+ )}
1410
+ </div>
1411
+ )}
1412
+ </div>
1413
+ )}
1414
+ </div>
1415
+ </div>
1416
+ </div>
1417
+ {requestType === RequestTypeEnum.ACCESS && (
1418
+ <PCSelect
1419
+ label={t('timePeriod')}
1420
+ options={timePeriodOptions}
1421
+ value={timePeriodOptions.find(
1422
+ (opt) => opt.value === timePeriod
1423
+ ) || null}
1424
+ onChange={(value) => {
1425
+ if (value) {
1426
+ setTimePeriod(value.value as string);
1427
+ }
1428
+ }}
1429
+ disabled={isPopup && !isPopupFormEnabled}
1430
+ />
1431
+ )}
1432
+ <PCTextarea
1433
+ label={t('additionalNote')}
1434
+ placeholder={t('reasonOptional')}
1435
+ value={formData.additionalNote}
1436
+ onChange={(e) =>
1437
+ setFormData({
1438
+ ...formData,
1439
+ additionalNote: e.target.value,
1440
+ })
1441
+ }
1442
+ style={isPopup ? { minHeight: '60px' } : {}}
1443
+ disabled={isPopup && !isPopupFormEnabled}
1444
+ />
1445
+ <PCCheckbox
1446
+ size="sm"
1447
+ label={
1448
+ <span>
1449
+ {t('confirmCheckBox')}{' '}
1450
+ <span style={{ color: getVar('--pc-error') }}>*</span>
1451
+ </span>
1452
+ }
1453
+ checked={formData.confirmCheckBox}
1454
+ onChange={(e) =>
1455
+ setFormData({
1456
+ ...formData,
1457
+ confirmCheckBox: e.target.checked,
1458
+ })
1459
+ }
1460
+ disabled={isPopup && !isPopupFormEnabled}
1461
+ />
1462
+ <div
1463
+ style={{
1464
+ ...pcStyles.flex,
1465
+ ...pcStyles.flexRow,
1466
+ ...pcStyles.justifyEnd,
1467
+ gap: getVar('--pc-spacing-sm'),
1468
+ ...(isPopup ? {
1469
+ position: 'sticky',
1470
+ bottom: 0,
1471
+ left: 0,
1472
+ right: 0,
1473
+ zIndex: 20,
1474
+ borderTop: `1px solid ${getVar('--pc-border-color')}`,
1475
+ backgroundColor: getVar('--pc-bg-primary'),
1476
+ padding: '0.75rem',
1477
+ marginLeft: '-0.75rem',
1478
+ marginRight: '-0.75rem',
1479
+ } : {
1480
+ marginTop: pcStyles.mtMd.marginTop,
1481
+ }),
1482
+ }}
1483
+ className="pc-flex-responsive pc-spacing-responsive"
1484
+ >
1485
+ {isPopup ? (
1486
+ <PCButton variant="outline" onClick={onClose} style={{ flexShrink: 0 }}>
1487
+ {t('cancel') ?? 'Cancel'}
1488
+ </PCButton>
1489
+ ) : (
1490
+ <PCButton variant="outline" style={{ flexShrink: 0 }}>
1491
+ {t('saveDraft')}
1492
+ </PCButton>
1493
+ )}
1494
+ <PCButton
1495
+ variant="primary"
1496
+ onClick={handleSubmitClick}
1497
+ disabled={!isFormValid || isLoading}
1498
+ isLoading={isLoading}
1499
+ style={{
1500
+ ...pcStyles.flex,
1501
+ ...pcStyles.itemsCenter,
1502
+ ...pcStyles.justifyCenter,
1503
+ minWidth: '120px',
1504
+ flexShrink: 0,
1505
+ opacity: (!isFormValid || isLoading) ? 0.5 : 1,
1506
+ cursor: (!isFormValid || isLoading) ? 'not-allowed' : (isLoading ? 'wait' : 'pointer'),
1507
+ }}
1508
+ >
1509
+ {t('submit')}
1510
+ </PCButton>
1511
+ </div>
1512
+ </div>
1513
+ </FormBlockWrapper>
1514
+ </div>
1515
+ </div>
1516
+ </div>
1517
+ )}
1518
+ <PCModal
1519
+ isOpen={isErasureConsentModalOpen}
1520
+ onClose={handleErasureModalClose}
1521
+ size="md"
1522
+ showCloseButton={false}
1523
+ footer={
1524
+ <div className="pc-erasure-modal-footer">
1525
+ <PCButton
1526
+ variant="outline"
1527
+ onClick={() => handleErasureModalChoice(false)}
1528
+ disabled={isLoading}
1529
+ >
1530
+ {t('revokeConsentNo')}
1531
+ </PCButton>
1532
+ <PCButton
1533
+ variant="primary"
1534
+ onClick={() => handleErasureModalChoice(true)}
1535
+ disabled={isLoading}
1536
+ isLoading={isLoading}
1537
+ >
1538
+ {t('revokeConsentYes')}
1539
+ </PCButton>
1540
+ </div>
1541
+ }
1542
+ >
1543
+ <div className="pc-erasure-modal-content">
1544
+ <div className="pc-erasure-modal-header">
1545
+ <div className="pc-erasure-modal-icon">
1546
+ <FiAlertCircle size={24} />
1547
+ </div>
1548
+ <p className="pc-erasure-modal-title">
1549
+ {t('revokeConsentModalTitle')}
1550
+ </p>
1551
+ <p className="pc-erasure-modal-question">
1552
+ {t('revokeConsentModalQuestion')}
1553
+ </p>
1554
+ </div>
1555
+
1556
+ <div className="pc-erasure-modal-purposes">
1557
+ <p className="pc-erasure-modal-purposes-label">
1558
+ {t('revokeConsentModalPurposesLabel')}
1559
+ </p>
1560
+ {selectedErasurePurposeList.length > 0 ? (
1561
+ <>
1562
+ {selectedErasurePurposeList
1563
+ .slice(0, MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL)
1564
+ .map((name) => (
1565
+ <div key={name} className="pc-erasure-modal-purpose-item">
1566
+ {name}
1567
+ </div>
1568
+ ))}
1569
+ {selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL && (
1570
+ <span className="pc-erasure-modal-purposes-placeholder">
1571
+ {t('revokeConsentModalAndMore', {
1572
+ count: selectedErasurePurposeList.length - MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL,
1573
+ })}
1574
+ </span>
1575
+ )}
1576
+ </>
1577
+ ) : (
1578
+ <span className="pc-erasure-modal-purposes-placeholder">
1579
+ {t('revokeConsentModalYourPurposes')}
1580
+ </span>
1581
+ )}
1582
+ </div>
1583
+
1584
+ <div className="pc-erasure-modal-warning">
1585
+ <p className="pc-erasure-modal-warning-text">
1586
+ <span className="pc-erasure-modal-warning-label">
1587
+ {t('revokeConsentModalIfYesLabel')}
1588
+ </span>{' '}
1589
+ {t('revokeConsentModalIfYesBody')}
1590
+ </p>
1591
+ </div>
1592
+ </div>
1593
+ </PCModal>
1594
+ </>
1595
+ );
1596
+ }