create-nextblock 0.15.8 → 0.15.9
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/package.json +1 -1
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -17
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -0
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -0
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -0
- package/templates/nextblock-template/app/actions/email.ts +241 -110
- package/templates/nextblock-template/app/actions/formActions.ts +245 -116
- package/templates/nextblock-template/app/actions/interactions.ts +489 -396
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -0
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -146
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +664 -1
- package/templates/nextblock-template/app/checkout/page.tsx +57 -52
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +552 -529
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -181
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -0
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -0
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -528
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -0
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -51
- package/templates/nextblock-template/app/cms/layout.tsx +101 -73
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -0
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -0
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -0
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -0
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -362
- package/templates/nextblock-template/app/cms/revisions/service.ts +20 -0
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -185
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -482
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -0
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -0
- package/templates/nextblock-template/app/thread/layout.tsx +15 -0
- package/templates/nextblock-template/app/thread/page.tsx +98 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -296
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -0
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -369
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -419
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -0
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -17
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -19
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -22
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -381
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -92
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -31
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -600
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -0
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -0
- package/templates/nextblock-template/docs/README.md +42 -41
- package/templates/nextblock-template/docs/assets/lighthouse-scores.png +0 -0
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +22 -2
- package/templates/nextblock-template/lib/blocks/blockColors.ts +44 -1
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -753
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +98 -0
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -0
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -0
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -254
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -0
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -0
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -0
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -0
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -0
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -0
- package/templates/nextblock-template/lib/messages/threads.ts +579 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +20 -0
- package/templates/nextblock-template/lib/site-url.test.ts +89 -0
- package/templates/nextblock-template/lib/site-url.ts +102 -48
- package/templates/nextblock-template/package.json +1 -1
|
@@ -1,381 +1,385 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useActionState, useState, useEffect, useRef } from 'react';
|
|
4
|
-
import { useFormStatus } from 'react-dom';
|
|
5
|
-
import Script from 'next/script';
|
|
6
|
-
import { handleFormSubmission } from '../../../app/actions/formActions';
|
|
7
|
-
import type { FormBlockContent, FormField } from '../../../lib/blocks/blockRegistry';
|
|
8
|
-
import { Button } from '@nextblock-cms/ui';
|
|
9
|
-
import { Checkbox } from '@nextblock-cms/ui';
|
|
10
|
-
import { Input } from '@nextblock-cms/ui';
|
|
11
|
-
import { Label } from '@nextblock-cms/ui';
|
|
12
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@nextblock-cms/ui';
|
|
13
|
-
import { Textarea } from '@nextblock-cms/ui';
|
|
14
|
-
import type { VisualEditAttributes } from '../../../lib/visual-editing/types';
|
|
15
|
-
|
|
16
|
-
interface FormBlockRendererProps {
|
|
17
|
-
content: FormBlockContent;
|
|
18
|
-
languageId: number;
|
|
19
|
-
visualEditAttributes?: VisualEditAttributes;
|
|
20
|
-
botProtectionPublic?: BotProtectionPublicSettings;
|
|
21
|
-
scriptNonce?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
|
|
25
|
-
|
|
26
|
-
type BotProtectionPublicSettings = {
|
|
27
|
-
provider: BotProtectionProvider;
|
|
28
|
-
siteKey: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function SubmitButton({ text, verifying }: { text: string; verifying?: boolean }) {
|
|
32
|
-
const { pending } = useFormStatus();
|
|
33
|
-
const isDisabled = pending || verifying;
|
|
34
|
-
return (
|
|
35
|
-
<Button type="submit" disabled={isDisabled}>
|
|
36
|
-
{verifying ? 'Verifying...' : pending ? 'Submitting...' : text}
|
|
37
|
-
</Button>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function resolveBotProtection(
|
|
42
|
-
content: FormBlockContent,
|
|
43
|
-
botProtectionPublic?: BotProtectionPublicSettings
|
|
44
|
-
) {
|
|
45
|
-
const blockProvider = content.botProtectionProvider;
|
|
46
|
-
const provider: BotProtectionProvider =
|
|
47
|
-
blockProvider === 'turnstile' || blockProvider === 'recaptcha'
|
|
48
|
-
? blockProvider
|
|
49
|
-
: botProtectionPublic?.provider || 'none';
|
|
50
|
-
const fallbackSiteKey =
|
|
51
|
-
provider === 'turnstile'
|
|
52
|
-
? process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
|
53
|
-
: provider === 'recaptcha'
|
|
54
|
-
? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY
|
|
55
|
-
: '';
|
|
56
|
-
const publicSiteKey =
|
|
57
|
-
provider === botProtectionPublic?.provider ? botProtectionPublic?.siteKey || '' : '';
|
|
58
|
-
const siteKey = content.botProtectionSiteKey?.trim() ||
|
|
59
|
-
publicSiteKey ||
|
|
60
|
-
fallbackSiteKey ||
|
|
61
|
-
'';
|
|
62
|
-
|
|
63
|
-
return { provider, siteKey };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const FormBlockRenderer: React.FC<FormBlockRendererProps> = ({ content, visualEditAttributes, botProtectionPublic, scriptNonce }) => {
|
|
67
|
-
const { provider, siteKey } = resolveBotProtection(content, botProtectionPublic);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
{
|
|
289
|
-
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useActionState, useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { useFormStatus } from 'react-dom';
|
|
5
|
+
import Script from 'next/script';
|
|
6
|
+
import { handleFormSubmission } from '../../../app/actions/formActions';
|
|
7
|
+
import type { FormBlockContent, FormField } from '../../../lib/blocks/blockRegistry';
|
|
8
|
+
import { Button } from '@nextblock-cms/ui';
|
|
9
|
+
import { Checkbox } from '@nextblock-cms/ui';
|
|
10
|
+
import { Input } from '@nextblock-cms/ui';
|
|
11
|
+
import { Label } from '@nextblock-cms/ui';
|
|
12
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@nextblock-cms/ui';
|
|
13
|
+
import { Textarea } from '@nextblock-cms/ui';
|
|
14
|
+
import type { VisualEditAttributes } from '../../../lib/visual-editing/types';
|
|
15
|
+
|
|
16
|
+
interface FormBlockRendererProps {
|
|
17
|
+
content: FormBlockContent;
|
|
18
|
+
languageId: number;
|
|
19
|
+
visualEditAttributes?: VisualEditAttributes;
|
|
20
|
+
botProtectionPublic?: BotProtectionPublicSettings;
|
|
21
|
+
scriptNonce?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
|
|
25
|
+
|
|
26
|
+
type BotProtectionPublicSettings = {
|
|
27
|
+
provider: BotProtectionProvider;
|
|
28
|
+
siteKey: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function SubmitButton({ text, verifying }: { text: string; verifying?: boolean }) {
|
|
32
|
+
const { pending } = useFormStatus();
|
|
33
|
+
const isDisabled = pending || verifying;
|
|
34
|
+
return (
|
|
35
|
+
<Button type="submit" disabled={isDisabled}>
|
|
36
|
+
{verifying ? 'Verifying...' : pending ? 'Submitting...' : text}
|
|
37
|
+
</Button>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveBotProtection(
|
|
42
|
+
content: FormBlockContent,
|
|
43
|
+
botProtectionPublic?: BotProtectionPublicSettings
|
|
44
|
+
) {
|
|
45
|
+
const blockProvider = content.botProtectionProvider;
|
|
46
|
+
const provider: BotProtectionProvider =
|
|
47
|
+
blockProvider === 'turnstile' || blockProvider === 'recaptcha'
|
|
48
|
+
? blockProvider
|
|
49
|
+
: botProtectionPublic?.provider || 'none';
|
|
50
|
+
const fallbackSiteKey =
|
|
51
|
+
provider === 'turnstile'
|
|
52
|
+
? process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
|
53
|
+
: provider === 'recaptcha'
|
|
54
|
+
? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY
|
|
55
|
+
: '';
|
|
56
|
+
const publicSiteKey =
|
|
57
|
+
provider === botProtectionPublic?.provider ? botProtectionPublic?.siteKey || '' : '';
|
|
58
|
+
const siteKey = content.botProtectionSiteKey?.trim() ||
|
|
59
|
+
publicSiteKey ||
|
|
60
|
+
fallbackSiteKey ||
|
|
61
|
+
'';
|
|
62
|
+
|
|
63
|
+
return { provider, siteKey };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const FormBlockRenderer: React.FC<FormBlockRendererProps> = ({ content, visualEditAttributes, botProtectionPublic, scriptNonce }) => {
|
|
67
|
+
const { provider, siteKey } = resolveBotProtection(content, botProtectionPublic);
|
|
68
|
+
// Only the opaque key travels to the client. The address it resolves to is read
|
|
69
|
+
// server-side, so it is neither disclosed here nor forgeable in the request.
|
|
70
|
+
const [state, formAction] = useActionState(handleFormSubmission.bind(null, {
|
|
71
|
+
formKey: content.form_key,
|
|
72
|
+
botProtectionProvider: provider,
|
|
73
|
+
}), {
|
|
74
|
+
success: false,
|
|
75
|
+
message: '',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const [recaptchaToken, setRecaptchaToken] = useState<string>('');
|
|
79
|
+
const turnstileRef = useRef<HTMLDivElement>(null);
|
|
80
|
+
const turnstileWidgetIdRef = useRef<string | null>(null);
|
|
81
|
+
const pendingTurnstileFormRef = useRef<HTMLFormElement | null>(null);
|
|
82
|
+
const [turnstileToken, setTurnstileToken] = useState<string>('');
|
|
83
|
+
const [turnstileError, setTurnstileError] = useState<string>('');
|
|
84
|
+
const [isVerifyingTurnstile, setIsVerifyingTurnstile] = useState(false);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (provider === 'turnstile' && siteKey && typeof window !== 'undefined') {
|
|
88
|
+
let widgetId: string | null = null;
|
|
89
|
+
|
|
90
|
+
const renderWidget = () => {
|
|
91
|
+
if ((window as any).turnstile && turnstileRef.current) {
|
|
92
|
+
// Clear any stale children
|
|
93
|
+
turnstileRef.current.innerHTML = '';
|
|
94
|
+
try {
|
|
95
|
+
widgetId = (window as any).turnstile.render(turnstileRef.current, {
|
|
96
|
+
sitekey: siteKey,
|
|
97
|
+
theme: 'light',
|
|
98
|
+
execution: 'execute',
|
|
99
|
+
'response-field': false,
|
|
100
|
+
callback: (token: string) => {
|
|
101
|
+
setTurnstileToken(token);
|
|
102
|
+
setTurnstileError('');
|
|
103
|
+
setIsVerifyingTurnstile(false);
|
|
104
|
+
|
|
105
|
+
const form = pendingTurnstileFormRef.current;
|
|
106
|
+
if (form) {
|
|
107
|
+
pendingTurnstileFormRef.current = null;
|
|
108
|
+
const input = form.querySelector('input[name="cf-turnstile-response"]') as HTMLInputElement | null;
|
|
109
|
+
if (input) {
|
|
110
|
+
input.value = token;
|
|
111
|
+
}
|
|
112
|
+
form.requestSubmit();
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
'expired-callback': () => {
|
|
116
|
+
setTurnstileToken('');
|
|
117
|
+
},
|
|
118
|
+
'error-callback': () => {
|
|
119
|
+
setTurnstileToken('');
|
|
120
|
+
pendingTurnstileFormRef.current = null;
|
|
121
|
+
setIsVerifyingTurnstile(false);
|
|
122
|
+
setTurnstileError('Security verification could not be completed. Please try again.');
|
|
123
|
+
},
|
|
124
|
+
'timeout-callback': () => {
|
|
125
|
+
setTurnstileToken('');
|
|
126
|
+
pendingTurnstileFormRef.current = null;
|
|
127
|
+
setIsVerifyingTurnstile(false);
|
|
128
|
+
setTurnstileError('Security verification timed out. Please try again.');
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
turnstileWidgetIdRef.current = widgetId;
|
|
132
|
+
|
|
133
|
+
if (widgetId && pendingTurnstileFormRef.current) {
|
|
134
|
+
try {
|
|
135
|
+
(window as any).turnstile.execute(widgetId);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error("Turnstile execute error:", err);
|
|
138
|
+
pendingTurnstileFormRef.current = null;
|
|
139
|
+
setIsVerifyingTurnstile(false);
|
|
140
|
+
setTurnstileError('Security verification could not be started. Please try again.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error("Turnstile render error:", err);
|
|
145
|
+
pendingTurnstileFormRef.current = null;
|
|
146
|
+
setIsVerifyingTurnstile(false);
|
|
147
|
+
setTurnstileError('Security verification could not be loaded. Please refresh and try again.');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if ((window as any).turnstile) {
|
|
153
|
+
renderWidget();
|
|
154
|
+
} else {
|
|
155
|
+
const interval = setInterval(() => {
|
|
156
|
+
if ((window as any).turnstile) {
|
|
157
|
+
clearInterval(interval);
|
|
158
|
+
renderWidget();
|
|
159
|
+
}
|
|
160
|
+
}, 100);
|
|
161
|
+
return () => clearInterval(interval);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
if (widgetId && (window as any).turnstile) {
|
|
166
|
+
try {
|
|
167
|
+
(window as any).turnstile.remove(widgetId);
|
|
168
|
+
} catch {
|
|
169
|
+
// Ignore
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
turnstileWidgetIdRef.current = null;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}, [provider, siteKey]);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!state.success && state.message) {
|
|
179
|
+
setRecaptchaToken('');
|
|
180
|
+
|
|
181
|
+
if (provider === 'turnstile') {
|
|
182
|
+
setTurnstileToken('');
|
|
183
|
+
pendingTurnstileFormRef.current = null;
|
|
184
|
+
setIsVerifyingTurnstile(false);
|
|
185
|
+
const widgetId = turnstileWidgetIdRef.current;
|
|
186
|
+
if (widgetId && typeof window !== 'undefined' && (window as any).turnstile) {
|
|
187
|
+
try {
|
|
188
|
+
(window as any).turnstile.reset(widgetId);
|
|
189
|
+
} catch {
|
|
190
|
+
// Ignore reset failures; the next render can still create a fresh token.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, [provider, state.message, state.success]);
|
|
196
|
+
|
|
197
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
198
|
+
if (provider === 'turnstile' && siteKey) {
|
|
199
|
+
const widgetId = turnstileWidgetIdRef.current;
|
|
200
|
+
const turnstile = typeof window !== 'undefined' ? (window as any).turnstile : undefined;
|
|
201
|
+
const token = widgetId && turnstile
|
|
202
|
+
? turnstile.getResponse(widgetId)
|
|
203
|
+
: turnstileToken;
|
|
204
|
+
|
|
205
|
+
const input = e.currentTarget.querySelector('input[name="cf-turnstile-response"]') as HTMLInputElement | null;
|
|
206
|
+
if (token && input) {
|
|
207
|
+
input.value = token;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
|
|
213
|
+
if (!widgetId || !turnstile) {
|
|
214
|
+
pendingTurnstileFormRef.current = e.currentTarget;
|
|
215
|
+
setTurnstileError('');
|
|
216
|
+
setIsVerifyingTurnstile(true);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pendingTurnstileFormRef.current = e.currentTarget;
|
|
221
|
+
setTurnstileError('');
|
|
222
|
+
setIsVerifyingTurnstile(true);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
turnstile.execute(widgetId);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error("Turnstile execute error:", err);
|
|
228
|
+
pendingTurnstileFormRef.current = null;
|
|
229
|
+
setIsVerifyingTurnstile(false);
|
|
230
|
+
setTurnstileError('Security verification could not be started. Please try again.');
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (provider === 'recaptcha' && siteKey && !recaptchaToken) {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
const form = e.currentTarget;
|
|
238
|
+
if (typeof window !== 'undefined' && (window as any).grecaptcha) {
|
|
239
|
+
(window as any).grecaptcha.ready(() => {
|
|
240
|
+
(window as any).grecaptcha.execute(siteKey, { action: 'submit' }).then((token: string) => {
|
|
241
|
+
setRecaptchaToken(token);
|
|
242
|
+
const input = form.querySelector('input[name="g-recaptcha-response"]') as HTMLInputElement;
|
|
243
|
+
if (input) {
|
|
244
|
+
input.value = token;
|
|
245
|
+
}
|
|
246
|
+
form.requestSubmit();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (state.success) {
|
|
254
|
+
return (
|
|
255
|
+
<div
|
|
256
|
+
className="p-4 rounded-md bg-green-100 text-green-800 text-center"
|
|
257
|
+
{...visualEditAttributes}
|
|
258
|
+
>
|
|
259
|
+
{content.success_message}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<>
|
|
266
|
+
{provider === 'recaptcha' && siteKey && (
|
|
267
|
+
<Script
|
|
268
|
+
strategy="lazyOnload"
|
|
269
|
+
src={`https://www.google.com/recaptcha/api.js?render=${siteKey}`}
|
|
270
|
+
nonce={scriptNonce}
|
|
271
|
+
/>
|
|
272
|
+
)}
|
|
273
|
+
{provider === 'turnstile' && siteKey && (
|
|
274
|
+
<Script
|
|
275
|
+
strategy="afterInteractive"
|
|
276
|
+
src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
|
|
277
|
+
nonce={scriptNonce}
|
|
278
|
+
onError={() => {
|
|
279
|
+
pendingTurnstileFormRef.current = null;
|
|
280
|
+
setIsVerifyingTurnstile(false);
|
|
281
|
+
setTurnstileError('Security verification could not be loaded. Please refresh and try again.');
|
|
282
|
+
}}
|
|
283
|
+
/>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
<form
|
|
287
|
+
action={formAction}
|
|
288
|
+
onSubmit={handleSubmit}
|
|
289
|
+
className="space-y-4 my-6 container mx-auto"
|
|
290
|
+
{...visualEditAttributes}
|
|
291
|
+
>
|
|
292
|
+
{/* Invisible Honeypot input field */}
|
|
293
|
+
<div className="absolute opaque-0 opacity-0 w-0 h-0 pointer-events-none select-none" style={{ opacity: 0, width: 0, height: 0, zIndex: -1 }}>
|
|
294
|
+
<Label htmlFor="verification_secondary_email" className="sr-only">Do not fill this field</Label>
|
|
295
|
+
<Input
|
|
296
|
+
id="verification_secondary_email"
|
|
297
|
+
type="text"
|
|
298
|
+
name="verification_secondary_email"
|
|
299
|
+
tabIndex={-1}
|
|
300
|
+
autoComplete="off"
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{content.fields.map((field: FormField) => (
|
|
305
|
+
<div key={field.temp_id} className="space-y-2">
|
|
306
|
+
<Label htmlFor={field.temp_id}>
|
|
307
|
+
{field.label} {field.is_required && <span className="text-red-500">*</span>}
|
|
308
|
+
</Label>
|
|
309
|
+
{renderField(field)}
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
|
|
313
|
+
{provider === 'recaptcha' && (
|
|
314
|
+
<input type="hidden" name="g-recaptcha-response" value={recaptchaToken} readOnly />
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{provider === 'turnstile' && siteKey && (
|
|
318
|
+
<div className="my-4 flex justify-start">
|
|
319
|
+
<input type="hidden" name="cf-turnstile-response" value={turnstileToken} readOnly />
|
|
320
|
+
<div ref={turnstileRef}></div>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
|
|
324
|
+
{turnstileError && (
|
|
325
|
+
<p className="text-sm text-red-600">{turnstileError}</p>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{state.message && !state.success && (
|
|
329
|
+
<p className="text-sm text-red-600">{state.message}</p>
|
|
330
|
+
)}
|
|
331
|
+
<SubmitButton text={content.submit_button_text} verifying={isVerifyingTurnstile} />
|
|
332
|
+
</form>
|
|
333
|
+
</>
|
|
334
|
+
);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const renderField = (field: FormField) => {
|
|
338
|
+
const commonProps = {
|
|
339
|
+
id: field.temp_id,
|
|
340
|
+
// Keyed on the stable temp_id, not the label: renaming a field's label used to
|
|
341
|
+
// silently change its FormData key and orphan the value.
|
|
342
|
+
name: `f_${field.temp_id}`,
|
|
343
|
+
placeholder: field.placeholder || '',
|
|
344
|
+
required: field.is_required,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
switch (field.field_type) {
|
|
348
|
+
case 'textarea':
|
|
349
|
+
return <Textarea {...commonProps} />;
|
|
350
|
+
case 'select':
|
|
351
|
+
return (
|
|
352
|
+
<Select name={commonProps.name} required={field.is_required}>
|
|
353
|
+
<SelectTrigger id={commonProps.id}><SelectValue placeholder={field.placeholder || 'Select an option'} /></SelectTrigger>
|
|
354
|
+
<SelectContent>
|
|
355
|
+
{field.options?.map(opt => <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>)}
|
|
356
|
+
</SelectContent>
|
|
357
|
+
</Select>
|
|
358
|
+
);
|
|
359
|
+
case 'radio':
|
|
360
|
+
return (
|
|
361
|
+
<div className="space-y-2">
|
|
362
|
+
{field.options?.map(opt => (
|
|
363
|
+
<div key={opt.value} className="flex items-center gap-2">
|
|
364
|
+
<input type="radio" id={`${commonProps.id}-${opt.value}`} name={commonProps.name} value={opt.value} required={field.is_required} className="h-4 w-4"/>
|
|
365
|
+
<Label htmlFor={`${commonProps.id}-${opt.value}`}>{opt.label}</Label>
|
|
366
|
+
</div>
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
case 'checkbox':
|
|
371
|
+
return (
|
|
372
|
+
<div className="flex items-center gap-2">
|
|
373
|
+
<Checkbox id={commonProps.id} name={commonProps.name} required={field.is_required} />
|
|
374
|
+
<Label htmlFor={commonProps.id} className="font-normal">{field.placeholder || "I agree"}</Label>
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
case 'email':
|
|
378
|
+
return <Input type="email" {...commonProps} />;
|
|
379
|
+
case 'text':
|
|
380
|
+
default:
|
|
381
|
+
return <Input type="text" {...commonProps} />;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export default FormBlockRenderer;
|