@akinon/pz-theme 2.0.0-beta.21

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 (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +27 -0
  3. package/readme.md +23 -0
  4. package/src/blocks/accordion-block.tsx +136 -0
  5. package/src/blocks/block-renderer-registry.tsx +77 -0
  6. package/src/blocks/button-block.tsx +593 -0
  7. package/src/blocks/counter-block.tsx +348 -0
  8. package/src/blocks/divider-block.tsx +20 -0
  9. package/src/blocks/embed-block.tsx +208 -0
  10. package/src/blocks/group-block.tsx +116 -0
  11. package/src/blocks/hotspot-block.tsx +147 -0
  12. package/src/blocks/icon-block.tsx +230 -0
  13. package/src/blocks/image-block.tsx +142 -0
  14. package/src/blocks/image-gallery-block.tsx +269 -0
  15. package/src/blocks/input-block.tsx +123 -0
  16. package/src/blocks/link-block.tsx +216 -0
  17. package/src/blocks/lottie-block.tsx +325 -0
  18. package/src/blocks/map-block.tsx +89 -0
  19. package/src/blocks/slider-block.tsx +595 -0
  20. package/src/blocks/tab-block.tsx +10 -0
  21. package/src/blocks/text-block.tsx +52 -0
  22. package/src/blocks/video-block.tsx +122 -0
  23. package/src/components/action-toolbar.tsx +305 -0
  24. package/src/components/designer-overlay.tsx +74 -0
  25. package/src/components/with-designer-features.tsx +142 -0
  26. package/src/dynamic-font-loader.tsx +79 -0
  27. package/src/hooks/use-designer-features.tsx +100 -0
  28. package/src/hooks/use-visibility-context.ts +27 -0
  29. package/src/index.ts +21 -0
  30. package/src/placeholder-registry.ts +31 -0
  31. package/src/sections/before-after-section.tsx +245 -0
  32. package/src/sections/contact-form-section.tsx +564 -0
  33. package/src/sections/countdown-campaign-banner-section.tsx +433 -0
  34. package/src/sections/coupon-banner-section.tsx +710 -0
  35. package/src/sections/divider-section.tsx +62 -0
  36. package/src/sections/featured-product-spotlight-section.tsx +507 -0
  37. package/src/sections/find-in-store-section.tsx +1995 -0
  38. package/src/sections/hover-showcase-section.tsx +326 -0
  39. package/src/sections/image-hotspot-section.tsx +142 -0
  40. package/src/sections/installment-options-section.tsx +1065 -0
  41. package/src/sections/notification-banner-section.tsx +173 -0
  42. package/src/sections/order-tracking-lookup-section.tsx +1379 -0
  43. package/src/sections/posts-slider-section.tsx +472 -0
  44. package/src/sections/pre-order-launch-banner-section.tsx +687 -0
  45. package/src/sections/section-renderer-registry.tsx +89 -0
  46. package/src/sections/section-wrapper.tsx +135 -0
  47. package/src/sections/shipping-threshold-progress-section.tsx +586 -0
  48. package/src/sections/stats-counter-section.tsx +486 -0
  49. package/src/sections/tabs-section.tsx +578 -0
  50. package/src/theme-block.tsx +102 -0
  51. package/src/theme-page-context.tsx +27 -0
  52. package/src/theme-placeholder-client.tsx +218 -0
  53. package/src/theme-placeholder-wrapper.tsx +786 -0
  54. package/src/theme-placeholder.tsx +305 -0
  55. package/src/theme-section.tsx +1241 -0
  56. package/src/theme-settings-context.tsx +13 -0
  57. package/src/utils/index.ts +791 -0
  58. package/src/utils/iterator-utils.test.ts +224 -0
  59. package/src/utils/iterator-utils.ts +617 -0
  60. package/src/utils/page-context-discovery.ts +119 -0
  61. package/src/utils/publish-window.ts +86 -0
  62. package/src/utils/visibility-rules.ts +188 -0
@@ -0,0 +1,564 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import clsx from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { useForm, SubmitHandler } from 'react-hook-form';
7
+ import { yupResolver } from '@hookform/resolvers/yup';
8
+ import * as yup from 'yup';
9
+
10
+ import ThemeBlock, { Block } from '../theme-block';
11
+ import { useThemeSettingsContext } from '../theme-settings-context';
12
+ import {
13
+ getCSSStyles,
14
+ getResponsiveValue,
15
+ resolveThemeCssVariables
16
+ } from '../utils';
17
+ import { Section } from '../theme-section';
18
+ import { Modal } from '@akinon/next/components/modal';
19
+ import {
20
+ useGetContactSubjectsQuery,
21
+ useSendContactMutation
22
+ } from '@akinon/next/data/client/account';
23
+ import { ContactFormType } from '@akinon/next/types';
24
+
25
+ interface ContactFormSectionProps {
26
+ section: Section;
27
+ currentBreakpoint?: string;
28
+ placeholderId?: string;
29
+ isDesigner?: boolean;
30
+ selectedBlockId?: string | null;
31
+ }
32
+
33
+ const contactFormSchema = yup.object().shape({
34
+ full_name: yup.string().required('Bu alan zorunludur.'),
35
+ email: yup
36
+ .string()
37
+ .email('Geçerli bir e-posta adresi girin.')
38
+ .required('Bu alan zorunludur.'),
39
+ phone: yup
40
+ .string()
41
+ .transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
42
+ .length(11, 'Telefon numarası 11 haneli olmalıdır.')
43
+ .required('Bu alan zorunludur.'),
44
+ subject: yup.string().required('Bu alan zorunludur.'),
45
+ message: yup
46
+ .string()
47
+ .required('Bu alan zorunludur.')
48
+ .min(10, 'Mesaj en az 10 karakter olmalıdır.')
49
+ .label('message'),
50
+ order: yup
51
+ .string()
52
+ .nullable()
53
+ .notRequired()
54
+ .when('subject', {
55
+ is: (value: string) => value === '2',
56
+ then: yup.string().required('Bu alan zorunludur.')
57
+ }),
58
+ file: yup.mixed()
59
+ });
60
+
61
+ const readProperty = (
62
+ properties: Record<string, unknown> | undefined,
63
+ key: string,
64
+ fallback: string,
65
+ breakpoint: string
66
+ ): string => {
67
+ if (!properties) return fallback;
68
+ const raw = properties[key];
69
+ if (raw === undefined || raw === null) return fallback;
70
+ if (typeof raw === 'string') return raw;
71
+ if (typeof raw === 'object') {
72
+ const responsive = raw as Record<string, unknown>;
73
+ const picked =
74
+ responsive[breakpoint] ?? responsive.desktop ?? Object.values(responsive)[0];
75
+ return picked == null ? fallback : String(picked);
76
+ }
77
+ return String(raw);
78
+ };
79
+
80
+ const ContactFormSection: React.FC<ContactFormSectionProps> = ({
81
+ section,
82
+ currentBreakpoint = 'desktop',
83
+ placeholderId = '',
84
+ isDesigner = false,
85
+ selectedBlockId = null
86
+ }) => {
87
+ const themeSettings = useThemeSettingsContext();
88
+
89
+ // RTK Query hooks
90
+ const {
91
+ data: contactSubjects,
92
+ isLoading: subjectsLoading,
93
+ isSuccess: subjectsSuccess
94
+ } = useGetContactSubjectsQuery();
95
+
96
+ const [
97
+ sendContact,
98
+ { isSuccess: formSuccess, isLoading: isSubmitting, error: submitError }
99
+ ] = useSendContactMutation();
100
+
101
+ // Build filtered subjects
102
+ const filteredSubjects: { label: string; value: string }[] = [
103
+ { label: readProperty(section.properties, 'subject-placeholder', 'Konu seçiniz', currentBreakpoint), value: '' }
104
+ ];
105
+
106
+ if (subjectsSuccess && contactSubjects) {
107
+ contactSubjects.forEach((item) => {
108
+ filteredSubjects.push({ label: item.text, value: String(item.id) });
109
+ });
110
+ }
111
+
112
+ const [selectedSubject, setSelectedSubject] = useState<string | null>(null);
113
+
114
+ // Feedback modal
115
+ const [feedbackModal, setFeedbackModal] = useState({
116
+ open: false,
117
+ title: '',
118
+ message: ''
119
+ });
120
+
121
+ // react-hook-form
122
+ const {
123
+ register,
124
+ reset,
125
+ handleSubmit,
126
+ formState: { errors }
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ } = useForm<ContactFormType & Record<string, any>>({
129
+ resolver: yupResolver(contactFormSchema) as any
130
+ });
131
+
132
+ // Read section properties
133
+ const props = section.properties || {};
134
+ const fullNamePlaceholder = readProperty(props, 'full-name-placeholder', 'Adınız Soyadınız', currentBreakpoint);
135
+ const emailPlaceholder = readProperty(props, 'email-placeholder', 'E-posta adresiniz', currentBreakpoint);
136
+ const phonePlaceholder = readProperty(props, 'phone-placeholder', 'Telefon numaranız', currentBreakpoint);
137
+ const messagePlaceholder = readProperty(props, 'message-placeholder', 'Mesajınızı yazın...', currentBreakpoint);
138
+ const buttonText = readProperty(props, 'button-text', 'Gönder', currentBreakpoint);
139
+ const successMessage = readProperty(props, 'success-message', 'Mesajınız başarıyla gönderildi!', currentBreakpoint);
140
+ const errorMessageText = readProperty(props, 'error-message', 'Bir hata oluştu. Lütfen tekrar deneyin.', currentBreakpoint);
141
+ const showFileUploadRaw = getResponsiveValue(props['show-file-upload'], currentBreakpoint, true);
142
+ const showFileUpload =
143
+ showFileUploadRaw === false || showFileUploadRaw === 'false' ? false : true;
144
+ // Read section styles
145
+ const maxWidth = getResponsiveValue(
146
+ section.styles?.['max-width'],
147
+ currentBreakpoint,
148
+ 'normal'
149
+ );
150
+ const maxWidthClass =
151
+ maxWidth === 'narrow'
152
+ ? 'max-w-4xl'
153
+ : maxWidth === 'normal'
154
+ ? 'max-w-7xl'
155
+ : '';
156
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
157
+
158
+ const formGap = getResponsiveValue(section.styles?.['form-gap'], currentBreakpoint, 16);
159
+
160
+ const inputBg = resolveThemeCssVariables(
161
+ String(getResponsiveValue(section.styles?.['input-background-color'], currentBreakpoint, '#ffffff')),
162
+ themeSettings
163
+ );
164
+ const inputTextColor = resolveThemeCssVariables(
165
+ String(getResponsiveValue(section.styles?.['input-text-color'], currentBreakpoint, '#0f172a')),
166
+ themeSettings
167
+ );
168
+ const inputBorderColor = resolveThemeCssVariables(
169
+ String(getResponsiveValue(section.styles?.['input-border-color'], currentBreakpoint, '#cbd5e1')),
170
+ themeSettings
171
+ );
172
+ const inputBorderRadius = String(
173
+ getResponsiveValue(section.styles?.['input-border-radius'], currentBreakpoint, '8px')
174
+ );
175
+ const inputPadding = String(
176
+ getResponsiveValue(section.styles?.['input-padding'], currentBreakpoint, '14px 16px')
177
+ );
178
+ const buttonBg = resolveThemeCssVariables(
179
+ String(getResponsiveValue(section.styles?.['button-background-color'], currentBreakpoint, '#0f172a')),
180
+ themeSettings
181
+ );
182
+ const buttonTextColor = resolveThemeCssVariables(
183
+ String(getResponsiveValue(section.styles?.['button-text-color'], currentBreakpoint, '#ffffff')),
184
+ themeSettings
185
+ );
186
+ const buttonHoverBg = resolveThemeCssVariables(
187
+ String(getResponsiveValue(section.styles?.['button-hover-background-color'], currentBreakpoint, '#1e293b')),
188
+ themeSettings
189
+ );
190
+ const buttonBorderRadius = String(
191
+ getResponsiveValue(section.styles?.['button-border-radius'], currentBreakpoint, '8px')
192
+ );
193
+ const buttonPadding = String(
194
+ getResponsiveValue(section.styles?.['button-padding'], currentBreakpoint, '14px 32px')
195
+ );
196
+
197
+ const filteredStyles = Object.fromEntries(
198
+ Object.entries(section.styles || {}).filter(
199
+ ([key]) =>
200
+ ![
201
+ 'max-width',
202
+ 'form-gap',
203
+ 'input-background-color',
204
+ 'input-text-color',
205
+ 'input-border-color',
206
+ 'input-border-radius',
207
+ 'input-padding',
208
+ 'button-background-color',
209
+ 'button-text-color',
210
+ 'button-hover-background-color',
211
+ 'button-border-radius',
212
+ 'button-padding'
213
+ ].includes(key)
214
+ )
215
+ );
216
+
217
+ const sectionStyles = getCSSStyles(filteredStyles, themeSettings, currentBreakpoint);
218
+
219
+ const inputStyle: React.CSSProperties = {
220
+ backgroundColor: inputBg,
221
+ color: inputTextColor,
222
+ border: `1px solid ${inputBorderColor}`,
223
+ borderRadius: inputBorderRadius,
224
+ padding: inputPadding,
225
+ fontSize: '15px',
226
+ outline: 'none',
227
+ width: '100%',
228
+ fontFamily: 'inherit'
229
+ };
230
+
231
+ const buttonStyle: React.CSSProperties = {
232
+ backgroundColor: buttonBg,
233
+ color: buttonTextColor,
234
+ border: 'none',
235
+ borderRadius: buttonBorderRadius,
236
+ padding: buttonPadding,
237
+ fontSize: '15px',
238
+ fontWeight: 600,
239
+ cursor: 'pointer',
240
+ transition: 'all 0.2s ease',
241
+ fontFamily: 'inherit'
242
+ };
243
+
244
+ // Blocks rendering
245
+ const sortedBlocks = [...(section.blocks || [])]
246
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
247
+ .filter((block) => (isDesigner ? true : !block.hidden));
248
+
249
+ const contentGroup = sortedBlocks.find(
250
+ (block) => block.type === 'group' && block.label === 'Content Container'
251
+ );
252
+
253
+ const textBlocks: Block[] = [];
254
+ if (contentGroup?.blocks) {
255
+ contentGroup.blocks
256
+ .filter((b) => b.type === 'text')
257
+ .forEach((b) => textBlocks.push(b));
258
+ }
259
+
260
+ const postBlockAction = (type: string, blockId: string, label?: string) => {
261
+ if (!window.parent) return;
262
+ window.parent.postMessage(
263
+ {
264
+ type,
265
+ data: {
266
+ placeholderId,
267
+ sectionId: section.id,
268
+ blockId,
269
+ ...(label ? { label } : {})
270
+ }
271
+ },
272
+ '*'
273
+ );
274
+ };
275
+
276
+ const renderBlock = (block: Block) => (
277
+ <ThemeBlock
278
+ key={block.id}
279
+ block={block}
280
+ placeholderId={placeholderId}
281
+ sectionId={section.id}
282
+ isDesigner={isDesigner}
283
+ isSelected={selectedBlockId === block.id}
284
+ selectedBlockId={selectedBlockId}
285
+ currentBreakpoint={currentBreakpoint}
286
+ onMoveUp={() => postBlockAction('MOVE_BLOCK_UP', block.id)}
287
+ onMoveDown={() => postBlockAction('MOVE_BLOCK_DOWN', block.id)}
288
+ onDuplicate={() => postBlockAction('DUPLICATE_BLOCK', block.id)}
289
+ onToggleVisibility={() => postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)}
290
+ onDelete={() => postBlockAction('DELETE_BLOCK', block.id)}
291
+ onRename={(newLabel) => postBlockAction('RENAME_BLOCK', block.id, newLabel)}
292
+ />
293
+ );
294
+
295
+ // Form submit handler — uses useSendContactMutation
296
+ const onSubmit: SubmitHandler<ContactFormType> = (data) => {
297
+ if (isDesigner) return;
298
+
299
+ const formData = new FormData();
300
+
301
+ Object.keys(data ?? {}).forEach((key) => {
302
+ if (key === 'file' && data[key]) {
303
+ formData.append(key, (data[key] as FileList)[0]);
304
+ } else if (data[key] && key !== 'file') {
305
+ formData.append(key, String(data[key]));
306
+ }
307
+ });
308
+
309
+ sendContact(formData)
310
+ .unwrap()
311
+ .then(() => {
312
+ setFeedbackModal({
313
+ open: true,
314
+ title: 'Başarılı',
315
+ message: successMessage
316
+ });
317
+ reset();
318
+ setSelectedSubject(null);
319
+ })
320
+ .catch((err) => {
321
+ let msg = errorMessageText;
322
+ if (err?.data && typeof err.data === 'object') {
323
+ const entries = Object.entries(err.data)
324
+ .map(([, msgs]) =>
325
+ Array.isArray(msgs) ? msgs.join(', ') : String(msgs)
326
+ );
327
+ if (entries.length > 0) msg = entries.join(' ');
328
+ }
329
+ setFeedbackModal({ open: true, title: 'Hata', message: msg });
330
+ });
331
+ };
332
+
333
+ const handleSubjectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
334
+ setSelectedSubject(e.target.value);
335
+ };
336
+
337
+ const isMobile = currentBreakpoint === 'mobile';
338
+
339
+ const errorStyle: React.CSSProperties = {
340
+ color: '#ef4444',
341
+ fontSize: '12px',
342
+ marginTop: '4px'
343
+ };
344
+
345
+
346
+ return (
347
+ <>
348
+ <div
349
+ className={twMerge(
350
+ clsx(
351
+ 'contact-form-section relative z-10 w-full',
352
+ hasMaxWidth && 'mx-auto',
353
+ maxWidthClass
354
+ )
355
+ )}
356
+ style={sectionStyles}
357
+ data-contact-form="true"
358
+ >
359
+ <div
360
+ style={{
361
+ display: 'flex',
362
+ flexDirection: 'column',
363
+ gap: '20px',
364
+ maxWidth: '600px',
365
+ width: '100%'
366
+ }}
367
+ >
368
+ {/* Render heading and description text blocks */}
369
+ {textBlocks.map(renderBlock)}
370
+
371
+ {/* Form */}
372
+ <form
373
+ onSubmit={handleSubmit(onSubmit)}
374
+ style={{ display: 'flex', flexDirection: 'column', gap: `${formGap}px` }}
375
+ >
376
+ {/* Name + Email row */}
377
+ <div
378
+ style={{
379
+ display: 'flex',
380
+ flexDirection: isMobile ? 'column' : 'row',
381
+ gap: `${formGap}px`
382
+ }}
383
+ >
384
+ <div style={{ flex: 1 }}>
385
+ <input
386
+ type="text"
387
+ placeholder={fullNamePlaceholder}
388
+ {...register('full_name')}
389
+ style={{
390
+ ...inputStyle,
391
+ borderColor: errors.full_name ? '#ef4444' : inputBorderColor
392
+ }}
393
+ />
394
+ {errors.full_name && (
395
+ <div style={errorStyle}>{errors.full_name.message}</div>
396
+ )}
397
+ </div>
398
+ <div style={{ flex: 1 }}>
399
+ <input
400
+ type="email"
401
+ placeholder={emailPlaceholder}
402
+ {...register('email')}
403
+ style={{
404
+ ...inputStyle,
405
+ borderColor: errors.email ? '#ef4444' : inputBorderColor
406
+ }}
407
+ />
408
+ {errors.email && (
409
+ <div style={errorStyle}>{errors.email.message}</div>
410
+ )}
411
+ </div>
412
+ </div>
413
+
414
+ {/* Phone */}
415
+ <div>
416
+ <input
417
+ type="tel"
418
+ placeholder={phonePlaceholder}
419
+ {...register('phone')}
420
+ style={{
421
+ ...inputStyle,
422
+ borderColor: errors.phone ? '#ef4444' : inputBorderColor
423
+ }}
424
+ />
425
+ {errors.phone && (
426
+ <div style={errorStyle}>{errors.phone.message}</div>
427
+ )}
428
+ </div>
429
+
430
+ {/* Subject — select from API */}
431
+ <div style={{ position: 'relative' }}>
432
+ {subjectsLoading && (
433
+ <div style={{ padding: '10px', fontSize: '13px', color: '#94a3b8' }}>
434
+ Konular yükleniyor...
435
+ </div>
436
+ )}
437
+ {subjectsSuccess && (
438
+ <select
439
+ {...register('subject')}
440
+ onChange={(e) => {
441
+ register('subject').onChange(e);
442
+ handleSubjectChange(e);
443
+ }}
444
+ style={{
445
+ ...inputStyle,
446
+ appearance: 'none',
447
+ WebkitAppearance: 'none',
448
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
449
+ backgroundRepeat: 'no-repeat',
450
+ backgroundPosition: 'right 16px center',
451
+ paddingRight: '40px',
452
+ borderColor: errors.subject ? '#ef4444' : inputBorderColor
453
+ }}
454
+ >
455
+ {filteredSubjects.map((opt) => (
456
+ <option key={opt.value} value={opt.value}>
457
+ {opt.label}
458
+ </option>
459
+ ))}
460
+ </select>
461
+ )}
462
+ {errors.subject && (
463
+ <div style={errorStyle}>{errors.subject.message}</div>
464
+ )}
465
+ </div>
466
+
467
+ {/* Message */}
468
+ <div>
469
+ <textarea
470
+ placeholder={messagePlaceholder}
471
+ rows={5}
472
+ {...register('message')}
473
+ style={{
474
+ ...inputStyle,
475
+ minHeight: '120px',
476
+ resize: 'vertical' as const,
477
+ borderColor: errors.message ? '#ef4444' : inputBorderColor
478
+ }}
479
+ />
480
+ {errors.message && (
481
+ <div style={errorStyle}>{errors.message.message}</div>
482
+ )}
483
+ </div>
484
+
485
+ {/* File upload */}
486
+ {showFileUpload && (
487
+ <div>
488
+ <input
489
+ type="file"
490
+ // @ts-ignore - react-hook-form FileList type issue
491
+ {...register('file')}
492
+ style={{
493
+ ...inputStyle,
494
+ cursor: 'pointer'
495
+ }}
496
+ />
497
+ </div>
498
+ )}
499
+
500
+ {/* Submit button */}
501
+ <div>
502
+ <button
503
+ type="submit"
504
+ disabled={isSubmitting}
505
+ style={{
506
+ ...buttonStyle,
507
+ opacity: isSubmitting ? 0.7 : 1
508
+ }}
509
+ onMouseEnter={(e) => {
510
+ if (!isSubmitting) {
511
+ (e.currentTarget as HTMLButtonElement).style.backgroundColor =
512
+ buttonHoverBg;
513
+ }
514
+ }}
515
+ onMouseLeave={(e) => {
516
+ (e.currentTarget as HTMLButtonElement).style.backgroundColor =
517
+ buttonBg;
518
+ }}
519
+ >
520
+ {isSubmitting ? 'Gönderiliyor...' : buttonText}
521
+ </button>
522
+ </div>
523
+
524
+ {/* Server-side errors */}
525
+ {submitError && (
526
+ <div style={{ color: '#ef4444', fontSize: '13px', marginTop: '4px' }}>
527
+ {'data' in submitError &&
528
+ submitError.data &&
529
+ typeof submitError.data === 'object'
530
+ ? Object.entries(submitError.data as Record<string, unknown>).map(
531
+ ([field, messages]) => (
532
+ <div key={field}>
533
+ {Array.isArray(messages)
534
+ ? messages.join(', ')
535
+ : String(messages)}
536
+ </div>
537
+ )
538
+ )
539
+ : errorMessageText}
540
+ </div>
541
+ )}
542
+ </form>
543
+ </div>
544
+ </div>
545
+
546
+ <Modal
547
+ portalId={`contact-form-feedback-${section.id}`}
548
+ open={feedbackModal.open}
549
+ setOpen={(open) =>
550
+ setFeedbackModal((prev) => ({ ...prev, open }))
551
+ }
552
+ title={feedbackModal.title}
553
+ showCloseButton={true}
554
+ className="w-[92vw] max-w-[420px] rounded"
555
+ >
556
+ <div className="p-6 text-sm leading-6 text-center text-black">
557
+ {feedbackModal.message}
558
+ </div>
559
+ </Modal>
560
+ </>
561
+ );
562
+ };
563
+
564
+ export default ContactFormSection;