@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.
- package/CHANGELOG.md +17 -0
- package/package.json +27 -0
- package/readme.md +23 -0
- package/src/blocks/accordion-block.tsx +136 -0
- package/src/blocks/block-renderer-registry.tsx +77 -0
- package/src/blocks/button-block.tsx +593 -0
- package/src/blocks/counter-block.tsx +348 -0
- package/src/blocks/divider-block.tsx +20 -0
- package/src/blocks/embed-block.tsx +208 -0
- package/src/blocks/group-block.tsx +116 -0
- package/src/blocks/hotspot-block.tsx +147 -0
- package/src/blocks/icon-block.tsx +230 -0
- package/src/blocks/image-block.tsx +142 -0
- package/src/blocks/image-gallery-block.tsx +269 -0
- package/src/blocks/input-block.tsx +123 -0
- package/src/blocks/link-block.tsx +216 -0
- package/src/blocks/lottie-block.tsx +325 -0
- package/src/blocks/map-block.tsx +89 -0
- package/src/blocks/slider-block.tsx +595 -0
- package/src/blocks/tab-block.tsx +10 -0
- package/src/blocks/text-block.tsx +52 -0
- package/src/blocks/video-block.tsx +122 -0
- package/src/components/action-toolbar.tsx +305 -0
- package/src/components/designer-overlay.tsx +74 -0
- package/src/components/with-designer-features.tsx +142 -0
- package/src/dynamic-font-loader.tsx +79 -0
- package/src/hooks/use-designer-features.tsx +100 -0
- package/src/hooks/use-visibility-context.ts +27 -0
- package/src/index.ts +21 -0
- package/src/placeholder-registry.ts +31 -0
- package/src/sections/before-after-section.tsx +245 -0
- package/src/sections/contact-form-section.tsx +564 -0
- package/src/sections/countdown-campaign-banner-section.tsx +433 -0
- package/src/sections/coupon-banner-section.tsx +710 -0
- package/src/sections/divider-section.tsx +62 -0
- package/src/sections/featured-product-spotlight-section.tsx +507 -0
- package/src/sections/find-in-store-section.tsx +1995 -0
- package/src/sections/hover-showcase-section.tsx +326 -0
- package/src/sections/image-hotspot-section.tsx +142 -0
- package/src/sections/installment-options-section.tsx +1065 -0
- package/src/sections/notification-banner-section.tsx +173 -0
- package/src/sections/order-tracking-lookup-section.tsx +1379 -0
- package/src/sections/posts-slider-section.tsx +472 -0
- package/src/sections/pre-order-launch-banner-section.tsx +687 -0
- package/src/sections/section-renderer-registry.tsx +89 -0
- package/src/sections/section-wrapper.tsx +135 -0
- package/src/sections/shipping-threshold-progress-section.tsx +586 -0
- package/src/sections/stats-counter-section.tsx +486 -0
- package/src/sections/tabs-section.tsx +578 -0
- package/src/theme-block.tsx +102 -0
- package/src/theme-page-context.tsx +27 -0
- package/src/theme-placeholder-client.tsx +218 -0
- package/src/theme-placeholder-wrapper.tsx +786 -0
- package/src/theme-placeholder.tsx +305 -0
- package/src/theme-section.tsx +1241 -0
- package/src/theme-settings-context.tsx +13 -0
- package/src/utils/index.ts +791 -0
- package/src/utils/iterator-utils.test.ts +224 -0
- package/src/utils/iterator-utils.ts +617 -0
- package/src/utils/page-context-discovery.ts +119 -0
- package/src/utils/publish-window.ts +86 -0
- 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;
|