@phygitallabs/phygital-consent 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.
@@ -1,40 +1,40 @@
1
1
  "use client";
2
2
 
3
3
  import { useState } from "react";
4
+ import { COOKIE_POLICY_URL, OTHER_INFO_URL } from "../env/constant";
4
5
  import { setConsentPreferenceCookie } from "../helpers/cookie";
5
6
  import { useCreateDeviceCookieConsent } from "../hooks/useConsent";
6
7
 
8
+ import { usePhygitalConsent } from "../provider/PhygitalConsentProvider";
9
+
7
10
  const PREFERENCES = ["essential", "analytics", "advertising"] as const;
8
11
  type PreferenceKey = (typeof PREFERENCES)[number];
9
12
 
10
13
  const PREFERENCE_CONFIG: Record<
11
14
  PreferenceKey,
12
- { label: string; description: string; disabled?: boolean }
15
+ { label: string; description: string; labelVi: string; descriptionVi: string; disabled?: boolean }
13
16
  > = {
14
17
  essential: {
15
18
  label: "Essential",
16
19
  description: "Required for the site to work (e.g. security, preferences).",
17
- disabled: true,
20
+ labelVi: "Cần thiết",
21
+ descriptionVi: "Cần thiết cho hoạt động của trang (bảo mật, tùy chọn).",
18
22
  },
19
23
  analytics: {
20
24
  label: "Analytics",
21
25
  description: "Help us improve by collecting anonymous usage data.",
26
+ labelVi: "Phân tích",
27
+ descriptionVi: "Giúp chúng tôi cải thiện bằng cách thu thập dữ liệu sử dụng ẩn danh.",
22
28
  },
23
29
  advertising: {
24
30
  label: "Advertising",
25
31
  description: "Used to show you relevant ads and measure campaigns.",
32
+ labelVi: "Quảng cáo",
33
+ descriptionVi: "Dùng để hiển thị quảng cáo phù hợp và đo lường chiến dịch.",
26
34
  },
27
35
  };
28
36
 
29
- const MOCK_CONTENT = {
30
- title: "Cookie preferences",
31
- description:
32
- "We use cookies to improve your experience. Choose which categories to enable, then save.",
33
- };
34
-
35
37
  export interface CookieConsentBannerProps {
36
- /** Device ID (consumer app must provide; will be hashed by app before API if required). */
37
- deviceId: string;
38
38
  /** Called after consent is submitted successfully (Reject or Accept). */
39
39
  onSubmitted?: () => void;
40
40
  /** Called when the banner is dismissed (e.g. hide from UI). */
@@ -44,24 +44,28 @@ export interface CookieConsentBannerProps {
44
44
  }
45
45
 
46
46
  export function CookieConsentBanner({
47
- deviceId,
48
47
  onSubmitted,
49
48
  onDismiss,
50
49
  className = "",
51
50
  }: CookieConsentBannerProps) {
52
51
  const [dismissed, setDismissed] = useState(false);
52
+ const [showCustomModal, setShowCustomModal] = useState(false);
53
+ const [expandedKey, setExpandedKey] = useState<PreferenceKey | null>(null);
53
54
  const [preferences, setPreferences] = useState<Record<PreferenceKey, boolean>>({
54
55
  essential: true,
55
- analytics: false,
56
- advertising: false,
56
+ analytics: true,
57
+ advertising: true,
57
58
  });
58
59
 
60
+ const { deviceId } = usePhygitalConsent();
61
+
59
62
  const createConsent = useCreateDeviceCookieConsent({
60
63
  onSuccess: (_data, variables) => {
61
64
  setConsentPreferenceCookie(deviceId, variables.selected_preferences ?? []);
62
65
  onSubmitted?.();
63
66
  onDismiss?.();
64
67
  setDismissed(true);
68
+ setShowCustomModal(false);
65
69
  },
66
70
  });
67
71
 
@@ -81,163 +85,404 @@ export function CookieConsentBanner({
81
85
  });
82
86
  };
83
87
 
84
- const handleSave = () => {
88
+ const handleAcceptAll = () => {
89
+ createConsent.mutate({
90
+ device_id: deviceId,
91
+ status: "ACCEPTED",
92
+ selected_preferences: [...PREFERENCES],
93
+ });
94
+ };
95
+
96
+ const handleSaveCustom = () => {
85
97
  const selected = getSelectedPreferences();
86
98
  createConsent.mutate({
87
99
  device_id: deviceId,
88
100
  status: "ACCEPTED",
89
- selected_preferences: selected.length > 0 ? selected : [],
101
+ selected_preferences: selected.length > 0 ? selected : ["essential"],
90
102
  });
91
103
  };
92
104
 
93
105
  if (dismissed) return null;
94
106
 
95
107
  return (
96
- <div
97
- role="dialog"
98
- aria-label={MOCK_CONTENT.title}
99
- className={`consent:fixed consent:bottom-0 consent:left-0 consent:right-0 consent:z-50 consent:flex consent:flex-col consent:gap-4 consent:rounded-t-xl consent:border consent:border-b-0 consent:border-gray-200 consent:bg-white consent:p-4 consent:shadow-lg consent:md:left-4 consent:md:right-auto consent:md:bottom-4 consent:md:max-w-md consent:md:rounded-xl consent:md:border consent:md:border-gray-200 ${className}`}
100
- style={{
101
- position: "fixed",
102
- bottom: 0,
103
- left: 0,
104
- right: 0,
105
- zIndex: 50,
106
- display: "flex",
107
- flexDirection: "column",
108
- gap: "1rem",
109
- borderRadius: "0.75rem 0.75rem 0 0",
110
- borderWidth: "1px 1px 0 1px",
111
- borderColor: "#e5e7eb",
112
- backgroundColor: "#fff",
113
- padding: "1rem",
114
- boxShadow: "0 -4px 6px -1px rgb(0 0 0 / 0.1)",
115
- }}
116
- >
117
- <h2
118
- className="consent:text-lg consent:font-semibold consent:text-gray-900"
119
- style={{ fontSize: "1.125rem", fontWeight: 600, color: "#111827" }}
120
- >
121
- {MOCK_CONTENT.title}
122
- </h2>
123
- <p
124
- className="consent:text-sm consent:text-gray-600"
125
- style={{ fontSize: "0.875rem", color: "#4b5563" }}
108
+ <>
109
+ {/* Screen 1: Bottom banner */}
110
+ <div
111
+ role="dialog"
112
+ aria-label="Thông báo về cookie"
113
+ className={`consent:fixed consent:bottom-0 consent:left-0 consent:right-0 consent:z-50 consent:flex consent:flex-col consent:gap-4 consent:rounded-t-xl consent:border consent:border-b-0 consent:border-gray-200 consent:bg-white consent:p-4 consent:shadow-lg consent:md:left-4 consent:md:right-auto consent:md:bottom-4 consent:md:max-w-md consent:md:rounded-xl consent:md:border consent:md:border-gray-200 ${className}`}
114
+ style={{
115
+ position: "fixed",
116
+ bottom: 0,
117
+ left: 0,
118
+ right: 0,
119
+ zIndex: 50,
120
+ display: "flex",
121
+ flexDirection: "column",
122
+ gap: "1rem",
123
+ borderRadius: "0.75rem 0.75rem 0 0",
124
+ borderWidth: "1px 1px 0 1px",
125
+ borderColor: "#e5e7eb",
126
+ backgroundColor: "#fff",
127
+ padding: "1rem",
128
+ boxShadow: "0 -4px 6px -1px rgb(0 0 0 / 0.1)",
129
+ }}
126
130
  >
127
- {MOCK_CONTENT.description}
128
- </p>
129
- <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
130
- {PREFERENCES.map((key) => {
131
- const config = PREFERENCE_CONFIG[key];
132
- const checked = preferences[key];
133
- return (
134
- <div
135
- key={key}
131
+ <h2
132
+ className="consent:text-lg consent:font-semibold consent:text-gray-900"
133
+ style={{ fontSize: "1.125rem", fontWeight: 600, color: "#111827" }}
134
+ >
135
+ Thông báo về cookie
136
+ </h2>
137
+ <div
138
+ className="consent:text-sm consent:text-gray-600"
139
+ style={{ fontSize: "0.875rem", color: "#4b5563", display: "flex", flexDirection: "column", gap: "0.5rem" }}
140
+ >
141
+ <p>
142
+ TapQuest sử dụng trình theo dõi (như Cookie hoặc SDK) cho:
143
+ </p>
144
+ <ul style={{ margin: 0, paddingLeft: "1.25rem" }}>
145
+ {PREFERENCES.map((key) => (
146
+ <li key={key}>{PREFERENCE_CONFIG[key].labelVi}</li>
147
+ ))}
148
+ </ul>
149
+ <p>
150
+ Trước khi bạn đồng ý, chỉ những trình theo dõi hoàn toàn cần thiết mới được triển khai.
151
+ </p>
152
+ <p>
153
+ Nếu bạn chọn Chấp nhận tất cả, bạn đồng ý sử dụng tất cả trình theo dõi của chúng tôi.
154
+ </p>
155
+ <p>
156
+ Nếu bạn chọn Từ chối tất cả, bạn từ chối những trình theo dõi yêu cầu sự đồng ý của bạn và sẽ không có quyền truy cập vào các đề nghị hoặc nội dung được cá nhân hóa.
157
+ </p>
158
+ <p>
159
+ Nếu bạn chọn Tùy chọn, bạn có thể chọn xem bạn có đồng ý sử dụng trình theo dõi trên ứng dụng của chúng tôi hay không và ở mức độ nào.
160
+ </p>
161
+ <p>
162
+ Bạn có thể rút lại sự đồng ý của mình bất kỳ lúc nào bằng cách nhấp vào liên kết trong{" "}
163
+ {COOKIE_POLICY_URL ? (
164
+ <a
165
+ href={COOKIE_POLICY_URL}
166
+ target="_blank"
167
+ rel="noopener noreferrer"
168
+ className="consent:text-blue-600 consent:underline"
169
+ style={{ color: "#2563eb", textDecoration: "underline" }}
170
+ >
171
+ Chính sách cookie
172
+ </a>
173
+ ) : (
174
+ "Chính sách cookie"
175
+ )}{" "}
176
+ của chúng tôi.
177
+ </p>
178
+ <p>
179
+ <button
180
+ type="button"
181
+ onClick={() => setShowCustomModal(true)}
182
+ className="consent:text-blue-600 consent:underline consent:bg-transparent consent:border-0 consent:p-0 consent:cursor-pointer consent:font-inherit"
136
183
  style={{
137
- display: "flex",
138
- alignItems: "flex-start",
139
- justifyContent: "space-between",
140
- gap: "0.75rem",
184
+ color: "#2563eb",
185
+ textDecoration: "underline",
186
+ background: "none",
187
+ border: "none",
188
+ padding: 0,
189
+ cursor: "pointer",
190
+ font: "inherit",
141
191
  }}
142
192
  >
143
- <div style={{ flex: 1, minWidth: 0 }}>
144
- <strong
145
- className="consent:text-sm consent:text-gray-900"
146
- style={{ fontSize: "0.875rem", color: "#111827" }}
147
- >
148
- {config.label}
149
- </strong>
150
- <p
151
- className="consent:text-xs consent:text-gray-500"
152
- style={{ fontSize: "0.75rem", color: "#6b7280", margin: "0.25rem 0 0" }}
153
- >
154
- {config.description}
155
- </p>
156
- </div>
157
- <button
158
- type="button"
159
- role="switch"
160
- aria-checked={checked}
161
- aria-label={`${config.label} cookies ${checked ? "on" : "off"}`}
162
- disabled={config.disabled}
163
- onClick={() => togglePreference(key)}
164
- style={{
165
- flexShrink: 0,
166
- width: 44,
167
- height: 24,
168
- borderRadius: 12,
169
- border: "none",
170
- padding: 0,
171
- cursor: config.disabled ? "default" : "pointer",
172
- backgroundColor: config.disabled ? "#e5e7eb" : checked ? "#111827" : "#d1d5db",
173
- opacity: config.disabled ? 0.7 : 1,
174
- transition: "background-color 0.2s",
175
- }}
176
- >
177
- <span
178
- style={{
179
- display: "block",
180
- width: 20,
181
- height: 20,
182
- borderRadius: "50%",
183
- backgroundColor: "#fff",
184
- marginLeft: checked ? 22 : 2,
185
- marginTop: 2,
186
- transition: "margin-left 0.2s",
187
- boxShadow: "0 1px 2px rgb(0 0 0 / 0.2)",
188
- }}
189
- />
190
- </button>
191
- </div>
192
- );
193
- })}
194
- </div>
195
- <div
196
- className="consent:flex consent:flex-wrap consent:gap-2"
197
- style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}
198
- >
199
- <button
200
- type="button"
201
- onClick={handleReject}
202
- disabled={createConsent.isPending}
203
- className="consent:rounded-lg consent:border consent:border-gray-300 consent:bg-white consent:px-4 consent:py-2 consent:text-sm consent:font-medium consent:text-gray-700 consent:shadow-sm hover:consent:bg-gray-50 disabled:consent:opacity-50"
204
- style={{
205
- borderRadius: "0.5rem",
206
- border: "1px solid #d1d5db",
207
- background: "#fff",
208
- padding: "0.5rem 1rem",
209
- fontSize: "0.875rem",
210
- fontWeight: 500,
211
- color: "#374151",
212
- }}
193
+ Tuỳ chỉnh cookie.
194
+ </button>
195
+ </p>
196
+ </div>
197
+ <div
198
+ className="consent:flex consent:flex-col consent:gap-2"
199
+ style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}
213
200
  >
214
- Reject non-essential
215
- </button>
216
- <button
217
- type="button"
218
- onClick={handleSave}
219
- disabled={createConsent.isPending}
220
- className="consent:rounded-lg consent:bg-gray-900 consent:px-4 consent:py-2 consent:text-sm consent:font-medium consent:text-white consent:shadow-sm hover:consent:bg-gray-800 disabled:consent:opacity-50"
201
+ <div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
202
+ <button
203
+ type="button"
204
+ onClick={handleAcceptAll}
205
+ disabled={createConsent.isPending}
206
+ className="consent:rounded-lg consent:bg-gray-900 consent:px-4 consent:py-2 consent:text-sm consent:font-medium consent:text-white consent:shadow-sm hover:consent:bg-gray-800 disabled:consent:opacity-50"
207
+ style={{
208
+ borderRadius: "0.5rem",
209
+ background: "#111827",
210
+ padding: "0.5rem 1rem",
211
+ fontSize: "0.875rem",
212
+ fontWeight: 500,
213
+ color: "#fff",
214
+ border: "none",
215
+ cursor: "pointer",
216
+ }}
217
+ >
218
+ Chấp nhận tất cả
219
+ </button>
220
+ <button
221
+ type="button"
222
+ onClick={handleReject}
223
+ disabled={createConsent.isPending}
224
+ className="consent:rounded-lg consent:bg-gray-900 consent:px-4 consent:py-2 consent:text-sm consent:font-medium consent:text-white consent:shadow-sm hover:consent:bg-gray-800 disabled:consent:opacity-50"
225
+ style={{
226
+ borderRadius: "0.5rem",
227
+ background: "#111827",
228
+ padding: "0.5rem 1rem",
229
+ fontSize: "0.875rem",
230
+ fontWeight: 500,
231
+ color: "#fff",
232
+ border: "none",
233
+ cursor: "pointer",
234
+ }}
235
+ >
236
+ Từ chối tất cả
237
+ </button>
238
+ </div>
239
+ <button
240
+ type="button"
241
+ onClick={() => setShowCustomModal(true)}
242
+ className="consent:text-blue-600 consent:underline consent:bg-transparent consent:border-0 consent:p-0 consent:text-sm consent:cursor-pointer consent:font-inherit"
243
+ style={{
244
+ color: "#2563eb",
245
+ textDecoration: "underline",
246
+ background: "none",
247
+ border: "none",
248
+ padding: 0,
249
+ fontSize: "0.875rem",
250
+ cursor: "pointer",
251
+ font: "inherit",
252
+ alignSelf: "flex-start",
253
+ }}
254
+ >
255
+ Tuỳ chọn
256
+ </button>
257
+ </div>
258
+ {createConsent.isError && (
259
+ <p
260
+ className="consent:text-sm consent:text-red-600"
261
+ style={{ fontSize: "0.875rem", color: "#dc2626" }}
262
+ >
263
+ Đã xảy ra lỗi. Vui lòng thử lại.
264
+ </p>
265
+ )}
266
+ </div>
267
+
268
+ {/* Screen 2: Custom preferences modal */}
269
+ {showCustomModal && (
270
+ <div
271
+ role="dialog"
272
+ aria-modal="true"
273
+ aria-label="Trung tâm tuỳ chọn bảo mật"
221
274
  style={{
222
- borderRadius: "0.5rem",
223
- background: "#111827",
224
- padding: "0.5rem 1rem",
225
- fontSize: "0.875rem",
226
- fontWeight: 500,
227
- color: "#fff",
275
+ position: "fixed",
276
+ inset: 0,
277
+ zIndex: 60,
278
+ display: "flex",
279
+ alignItems: "center",
280
+ justifyContent: "center",
281
+ backgroundColor: "rgba(0,0,0,0.5)",
282
+ padding: "1rem",
228
283
  }}
284
+ onClick={(e) => e.target === e.currentTarget && setShowCustomModal(false)}
229
285
  >
230
- Save preferences
231
- </button>
232
- </div>
233
- {createConsent.isError && (
234
- <p
235
- className="consent:text-sm consent:text-red-600"
236
- style={{ fontSize: "0.875rem", color: "#dc2626" }}
237
- >
238
- Something went wrong. Please try again.
239
- </p>
286
+ <div
287
+ className="consent:bg-white consent:rounded-xl consent:border consent:border-gray-200 consent:shadow-lg consent:max-h-[90vh] consent:overflow-y-auto"
288
+ style={{
289
+ backgroundColor: "#fff",
290
+ borderRadius: "0.75rem",
291
+ border: "1px solid #e5e7eb",
292
+ boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)",
293
+ maxWidth: "28rem",
294
+ width: "100%",
295
+ maxHeight: "90vh",
296
+ overflowY: "auto",
297
+ padding: "1.25rem",
298
+ position: "relative",
299
+ }}
300
+ onClick={(e) => e.stopPropagation()}
301
+ >
302
+ <button
303
+ type="button"
304
+ onClick={() => setShowCustomModal(false)}
305
+ aria-label="Đóng"
306
+ style={{
307
+ position: "absolute",
308
+ top: "1rem",
309
+ right: "1rem",
310
+ width: "2rem",
311
+ height: "2rem",
312
+ borderRadius: "0.25rem",
313
+ border: "none",
314
+ background: "transparent",
315
+ cursor: "pointer",
316
+ fontSize: "1.25rem",
317
+ lineHeight: 1,
318
+ color: "#6b7280",
319
+ }}
320
+ >
321
+ ×
322
+ </button>
323
+ <h2
324
+ className="consent:text-lg consent:font-semibold consent:text-gray-900"
325
+ style={{ fontSize: "1.125rem", fontWeight: 600, color: "#111827", marginBottom: "0.75rem", paddingRight: "2rem" }}
326
+ >
327
+ Trung tâm tuỳ chọn bảo mật
328
+ </h2>
329
+ <div
330
+ className="consent:text-sm consent:text-gray-600"
331
+ style={{ fontSize: "0.875rem", color: "#4b5563", marginBottom: "1rem" }}
332
+ >
333
+ <p style={{ marginBottom: "0.5rem" }}>
334
+ Khi bạn truy cập bất kỳ trang web nào, trang web đó có thể lưu trữ hoặc truy xuất thông tin trên trình duyệt của bạn, chủ yếu dưới dạng cookie. Thông tin này có thể liên quan đến bạn, tùy chọn của bạn hoặc thiết bị của bạn, và chủ yếu được sử dụng để trang web hoạt động như mong đợi.
335
+ </p>
336
+ <p style={{ marginBottom: "0.5rem" }}>
337
+ Thông tin này thường không trực tiếp xác định bạn nhưng có thể mang lại trải nghiệm web được cá nhân hóa hơn. Chúng tôi tôn trọng quyền riêng tư của bạn; bạn có thể chọn không cho phép một số loại cookie. Nhấp vào tiêu đề từng danh mục để tìm hiểu thêm và thay đổi cài đặt mặc định. Việc chặn một số cookie có thể ảnh hưởng đến trải nghiệm của bạn với trang web và các dịch vụ được cung cấp.
338
+ </p>
339
+ <p style={{ marginBottom: "0.5rem" }}>
340
+ Nếu chọn "Không theo dõi" khi lần đầu sử dụng TapQuest, chỉ cookie hoàn toàn cần thiết mới được sử dụng.
341
+ </p>
342
+ {OTHER_INFO_URL ? (
343
+ <a
344
+ href={OTHER_INFO_URL}
345
+ target="_blank"
346
+ rel="noopener noreferrer"
347
+ className="consent:text-blue-600 consent:underline"
348
+ style={{ color: "#2563eb", textDecoration: "underline" }}
349
+ >
350
+ Thông tin khác
351
+ </a>
352
+ ) : (
353
+ <span style={{ color: "#2563eb", textDecoration: "underline" }}>Thông tin khác</span>
354
+ )}
355
+ </div>
356
+ <h3
357
+ className="consent:text-sm consent:font-semibold consent:text-gray-900"
358
+ style={{ fontSize: "0.875rem", fontWeight: 600, color: "#111827", marginBottom: "0.75rem" }}
359
+ >
360
+ Quản lý tuỳ chọn đồng ý
361
+ </h3>
362
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem", marginBottom: "1.25rem" }}>
363
+ {PREFERENCES.map((key) => {
364
+ const config = PREFERENCE_CONFIG[key];
365
+ const checked = preferences[key];
366
+ const isExpanded = expandedKey === key;
367
+ return (
368
+ <div
369
+ key={key}
370
+ style={{
371
+ border: "1px solid #e5e7eb",
372
+ borderRadius: "0.5rem",
373
+ overflow: "hidden",
374
+ }}
375
+ >
376
+ <div
377
+ style={{
378
+ display: "flex",
379
+ alignItems: "center",
380
+ gap: "0.5rem",
381
+ padding: "0.75rem",
382
+ cursor: "pointer",
383
+ backgroundColor: isExpanded ? "#f9fafb" : "transparent",
384
+ }}
385
+ onClick={() => setExpandedKey(isExpanded ? null : key)}
386
+ >
387
+ <span
388
+ style={{
389
+ flexShrink: 0,
390
+ width: "1.25rem",
391
+ height: "1.25rem",
392
+ display: "flex",
393
+ alignItems: "center",
394
+ justifyContent: "center",
395
+ fontSize: "1rem",
396
+ color: "#6b7280",
397
+ transform: isExpanded ? "rotate(45deg)" : "none",
398
+ transition: "transform 0.2s",
399
+ }}
400
+ >
401
+ +
402
+ </span>
403
+ <span
404
+ className="consent:font-medium consent:text-gray-900"
405
+ style={{ flex: 1, fontSize: "0.875rem", fontWeight: 500, color: "#111827" }}
406
+ >
407
+ {config.labelVi}
408
+ </span>
409
+ <button
410
+ type="button"
411
+ role="switch"
412
+ aria-checked={checked}
413
+ aria-label={`${config.labelVi} ${checked ? "bật" : "tắt"}`}
414
+ disabled={config.disabled}
415
+ onClick={(e) => {
416
+ e.stopPropagation();
417
+ togglePreference(key);
418
+ }}
419
+ style={{
420
+ flexShrink: 0,
421
+ width: 44,
422
+ height: 24,
423
+ borderRadius: 12,
424
+ border: "none",
425
+ padding: 0,
426
+ cursor: config.disabled ? "default" : "pointer",
427
+ backgroundColor: config.disabled ? "#e5e7eb" : checked ? "#111827" : "#d1d5db",
428
+ opacity: config.disabled ? 0.7 : 1,
429
+ transition: "background-color 0.2s",
430
+ }}
431
+ >
432
+ <span
433
+ style={{
434
+ display: "block",
435
+ width: 20,
436
+ height: 20,
437
+ borderRadius: "50%",
438
+ backgroundColor: "#fff",
439
+ marginLeft: checked ? 22 : 2,
440
+ marginTop: 2,
441
+ transition: "margin-left 0.2s",
442
+ boxShadow: "0 1px 2px rgb(0 0 0 / 0.2)",
443
+ }}
444
+ />
445
+ </button>
446
+ </div>
447
+ {isExpanded && (
448
+ <div
449
+ className="consent:text-sm consent:text-gray-600"
450
+ style={{
451
+ fontSize: "0.875rem",
452
+ color: "#4b5563",
453
+ padding: "0 0.75rem 0.75rem 2.5rem",
454
+ borderTop: "1px solid #e5e7eb",
455
+ }}
456
+ >
457
+ {config.descriptionVi}
458
+ </div>
459
+ )}
460
+ </div>
461
+ );
462
+ })}
463
+ </div>
464
+ <button
465
+ type="button"
466
+ onClick={handleSaveCustom}
467
+ disabled={createConsent.isPending}
468
+ className="consent:rounded-lg consent:bg-gray-900 consent:px-4 consent:py-2 consent:text-sm consent:font-medium consent:text-white consent:shadow-sm hover:consent:bg-gray-800 disabled:consent:opacity-50 consent:w-full"
469
+ style={{
470
+ borderRadius: "0.5rem",
471
+ background: "#111827",
472
+ padding: "0.5rem 1rem",
473
+ fontSize: "0.875rem",
474
+ fontWeight: 500,
475
+ color: "#fff",
476
+ border: "none",
477
+ cursor: "pointer",
478
+ width: "100%",
479
+ }}
480
+ >
481
+ Xác nhận lựa chọn của tôi
482
+ </button>
483
+ </div>
484
+ </div>
240
485
  )}
241
- </div>
486
+ </>
242
487
  );
243
488
  }
@@ -1 +1,7 @@
1
1
  export const CONSENT_API_BASE_URL = "";
2
+
3
+ /** URL to cookie policy page (e.g. for "Chính sách cookie" link). */
4
+ export const COOKIE_POLICY_URL = "";
5
+
6
+ /** URL to other information page (e.g. for "Thông tin khác" link in preferences modal). */
7
+ export const OTHER_INFO_URL = "";
@@ -1 +1,2 @@
1
1
  export * from "./cookie";
2
+ export * from "./sha256";