blocks-dusted 0.1.11 → 0.1.12

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 (46) hide show
  1. package/package.json +5 -2
  2. package/src/cli.js +39 -3
  3. package/src/commands/shop.js +59 -0
  4. package/src/commands/warehouse.js +73 -0
  5. package/templates/blocks/DD-BookCall/Component.tsx +535 -534
  6. package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -139
  7. package/templates/blocks/DD-Carousel-A/Component.tsx +1 -1
  8. package/templates/blocks/DD-Carousel-B/Component.tsx +1 -1
  9. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  10. package/templates/blocks/DD-ComparisonTable/Component.tsx +1 -1
  11. package/templates/blocks/DD-Contact/Component.tsx +8 -7
  12. package/templates/blocks/DD-Counter/Component.tsx +10 -10
  13. package/templates/blocks/DD-FeatCat/Component.tsx +1 -1
  14. package/templates/blocks/DD-FeatStrip/Component.tsx +1 -1
  15. package/templates/blocks/DD-Hero/Component.tsx +297 -320
  16. package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -938
  17. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  18. package/templates/blocks/DD-MasonaryMedia/Component.tsx +8 -10
  19. package/templates/blocks/DD-Pricing/Component.tsx +369 -372
  20. package/templates/blocks/DD-Process/Component.tsx +147 -149
  21. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  22. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  23. package/templates/blocks/DD-Section/Component.tsx +1 -1
  24. package/templates/blocks/DD-Services/Component.tsx +1 -1
  25. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +13 -13
  26. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  27. package/templates/blocks/DD-StackCards/Component.tsx +473 -473
  28. package/templates/blocks/DD-Team/Component.tsx +1 -1
  29. package/templates/blocks/DD-TechStack/Component.tsx +22 -28
  30. package/templates/blocks/DD-Testimonials/Component.tsx +35 -43
  31. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  32. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  33. package/templates/blocks/DD-Work/Component.tsx +4 -4
  34. package/templates/blocks/DD-Work-B/Component.tsx +1 -1
  35. package/templates/blocks/DDHG-CTASection2/Component.tsx +6 -6
  36. package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -243
  37. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -93
  38. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  39. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -213
  40. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  41. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  42. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +24 -28
  43. package/templates/blocks/DDHG-ServiceHero/Component.tsx +1 -1
  44. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  45. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  46. package/templates/blocks/DDHG-TeamCards/Component.tsx +169 -131
@@ -1,534 +1,535 @@
1
- 'use client'
2
- import React, { type FormEvent, useMemo, useState } from 'react'
3
- import * as LucideIcons from 'lucide-react'
4
- import {
5
- Check,
6
- Loader2,
7
- CalendarDays,
8
- Globe,
9
- Clock,
10
- AlertCircle,
11
- CalendarCheck
12
- } from 'lucide-react'
13
- import { getClientSideURL } from '@/utilities/getURL'
14
-
15
- type Status = 'idle' | 'submitting' | 'success' | 'error'
16
- export type DDBookCallProps = {
17
- eyebrow?: string | null
18
- headlineMain?: string | null
19
- headlineAccent?: string | null
20
- description?: string | null
21
- callTypes?: Array<{
22
- value: string
23
- label: string
24
- duration: string
25
- iconName?: string | null
26
- desc?: string | null
27
- }> | null
28
- features?: Array<{
29
- iconName?: string | null
30
- text: string
31
- }> | null
32
- form?: string | number | { id?: string | number } | null
33
- timeSlots?: Array<{ slot?: string | null }> | null
34
- footerNote?: string | null
35
- sectionId?: string | null
36
- customCSS?: string | null
37
- }
38
- function DynamicIcon({
39
- name,
40
- size = 16,
41
- ...props
42
- }: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
43
- if (!name) return null
44
- const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
45
- if (!Icon) return null
46
- return <Icon size={size} {...props} />
47
- }
48
-
49
- function getMinDate() {
50
- const d = new Date()
51
- d.setDate(d.getDate() + 1)
52
- return d.toISOString().split('T')[0]
53
- }
54
-
55
- function getMaxDate() {
56
- const d = new Date()
57
- d.setDate(d.getDate() + 60)
58
- return d.toISOString().split('T')[0]
59
- }
60
-
61
- export const DDBookCall: React.FC<DDBookCallProps> = ({
62
- eyebrow,
63
- headlineMain,
64
- headlineAccent,
65
- description,
66
- callTypes,
67
- features,
68
- form,
69
- timeSlots,
70
- footerNote,
71
- sectionId,
72
- customCSS
73
- }) => {
74
- const [status, setStatus] = useState<Status>('idle')
75
- const [selectedType, setSelectedType] = useState<string>(callTypes?.[0]?.value ?? 'discovery')
76
- const [selectedSlot, setSelectedSlot] = useState('')
77
- const [selectedDate, setSelectedDate] = useState('')
78
- const [charCount, setCharCount] = useState(0)
79
-
80
- const formID = typeof form === 'object' && form !== null ? form.id : form
81
-
82
- const formIdentifier = useMemo(() => {
83
- const safeSection = (sectionId || 'book-call').toLowerCase().replace(/[^a-z0-9-_]/g, '-')
84
- return `studio-book-call-${String(formID || 'unknown')}-${safeSection}`
85
- }, [formID, sectionId])
86
-
87
- const selectedCallType = callTypes?.find((t) => t.value === selectedType) ?? callTypes?.[0]
88
- const slots = timeSlots?.map((t) => t.slot).filter(Boolean) as string[]
89
-
90
- const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
91
- e.preventDefault()
92
- const formElement = e.currentTarget
93
-
94
- if (!selectedDate || !selectedSlot) return
95
-
96
- const notes = formElement.querySelector<HTMLTextAreaElement>('[name="notes"]')?.value ?? ''
97
- if (notes.length > 500) return
98
-
99
- setStatus('submitting')
100
-
101
- const data = new FormData(formElement)
102
-
103
- const submissionData = [
104
- { field: 'name', value: data.get('name') },
105
- { field: 'email', value: data.get('email') },
106
- { field: 'company', value: data.get('company') },
107
- { field: 'call_type', value: selectedType },
108
- { field: 'preferred_date', value: selectedDate },
109
- { field: 'preferred_time', value: selectedSlot },
110
- { field: 'notes', value: data.get('notes') }
111
- ]
112
-
113
- try {
114
- const res = await fetch(`${getClientSideURL()}/api/form-submissions`, {
115
- method: 'POST',
116
- headers: { 'Content-Type': 'application/json' },
117
- body: JSON.stringify({ form: formID, submissionData })
118
- })
119
-
120
- if (res.ok) {
121
- setStatus('success')
122
- formElement.reset()
123
- setSelectedSlot('')
124
- setSelectedDate('')
125
- setCharCount(0)
126
- } else {
127
- setStatus('error')
128
- }
129
- } catch {
130
- setStatus('error')
131
- }
132
- }
133
-
134
- return (
135
- <>
136
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
137
-
138
- <section
139
- id={sectionId ?? 'book-call'}
140
- className="container-fluid mx-auto py-28 relative overflow-hidden"
141
- >
142
- {/* Background gradients – light & dark variants */}
143
- <div
144
- className="absolute inset-0 pointer-events-none opacity-30
145
- bg-[radial-gradient(circle_at_20%_50%,rgba(0,0,0,0.06)_0%,transparent_50%),
146
- radial-gradient(circle_at_80%_20%,rgba(0,0,0,0.03)_0%,transparent_40%)]
147
- dark:bg-[radial-gradient(circle_at_20%_50%,rgba(110,231,183,0.06)_0%,transparent_50%),
148
- radial-gradient(circle_at_80%_20%,rgba(255,255,255,0.03)_0%,transparent_40%)]"
149
- />
150
- <div
151
- className="absolute inset-0 pointer-events-none
152
- bg-[linear-gradient(rgba(0,0,0,0.06)_1px,transparent_1px),
153
- linear-gradient(90deg,rgba(0,0,0,0.06)_1px,transparent_1px)]
154
- dark:bg-[linear-gradient(rgba(255,255,255,0.012)_1px,transparent_1px),
155
- linear-gradient(90deg,rgba(255,255,255,0.012)_1px,transparent_1px)]
156
- bg-size-[60px_60px]"
157
- />
158
-
159
- <div className="relative max-w-7xl lg:max-w-8xl mx-auto px-4 md:px-6">
160
- {/* Header */}
161
- <div className="text-center mb-16">
162
- {eyebrow && (
163
- <div className="inline-flex items-center gap-2 mb-4">
164
- <span className="w-1.5 h-1.5 rounded-full bg-lime-400 animate-pulse" />
165
- <span className="text-xs font-semibold tracking-widest uppercase text-stone-500 dark:text-stone-500">
166
- {eyebrow}
167
- </span>
168
- </div>
169
- )}
170
- <h2 className="text-4xl md:text-5xl font-bold text-stone-900 dark:text-stone-50 leading-tight mb-4">
171
- {headlineMain}
172
- <br />
173
- <span className="text-stone-600 dark:text-stone-400">{headlineAccent}</span>
174
- </h2>
175
- {description && (
176
- <p className="text-stone-600 dark:text-stone-500 text-base max-w-md mx-auto">
177
- {description}
178
- </p>
179
- )}
180
- </div>
181
- <div className="grid grid-cols-1 lg:grid-cols-5 gap-8 items-start">
182
- {/* Left sidebar */}
183
- <div className="lg:col-span-2 space-y-6">
184
- {/* Call type selector */}
185
- {callTypes && callTypes.length > 0 && (
186
- <div>
187
- <p className="text-xs font-semibold text-stone-500 dark:text-stone-500 uppercase tracking-widest mb-3">
188
- Select Session Type
189
- </p>
190
- <div className="space-y-2.5">
191
- {callTypes.map((type) => (
192
- <button
193
- key={type.value}
194
- type="button"
195
- onClick={() => setSelectedType(type.value)}
196
- className={`w-full flex items-center gap-4 p-4 rounded-xl border transition-all cursor-pointer text-left ${
197
- selectedType === type.value
198
- ? 'bg-stone-50/80 dark:bg-stone-50/[0.08] border-stone-300 dark:border-white/20 text-stone-900 dark:text-stone-50'
199
- : 'bg-stone-100/60 dark:bg-stone-900/50 border-stone-200 dark:border-stone-800 text-stone-600 dark:text-stone-400 hover:border-stone-300 dark:hover:border-stone-700 hover:text-stone-900 dark:hover:text-stone-300'
200
- }`}
201
- >
202
- <div
203
- className={`w-9 h-9 flex items-center justify-center rounded-lg shrink-0 transition-colors ${
204
- selectedType === type.value
205
- ? 'bg-lime-400/15 text-emerald-400'
206
- : 'bg-stone-200 dark:bg-stone-800 text-stone-500 dark:text-stone-500'
207
- }`}
208
- >
209
- <DynamicIcon
210
- className="transition-colors duration-300"
211
- name={type.iconName}
212
- size={15}
213
- />
214
- </div>
215
- <div className="flex-1 min-w-0">
216
- <div className="flex items-center justify-between gap-2">
217
- <span className="text-sm font-semibold">{type.label}</span>
218
- <span
219
- className={`text-xs font-medium px-2 py-0.5 rounded-full whitespace-nowrap ${
220
- selectedType === type.value
221
- ? 'bg-lime-400/15 text-emerald-400'
222
- : 'bg-stone-200 dark:bg-stone-800 text-stone-500 dark:text-stone-500'
223
- }`}
224
- >
225
- {type.duration}
226
- </span>
227
- </div>
228
- {type.desc && <p className="text-xs mt-0.5 opacity-70">{type.desc}</p>}
229
- </div>
230
- </button>
231
- ))}
232
- </div>
233
- </div>
234
- )}
235
-
236
- {/* What to expect */}
237
- {features && features.length > 0 && (
238
- <div className="rounded-xl bg-stone-100/60 dark:bg-stone-900/50 border border-stone-200 dark:border-stone-800 p-5">
239
- <p className="text-xs font-semibold text-stone-500 dark:text-stone-500 uppercase tracking-widest mb-4">
240
- What to expect
241
- </p>
242
- <div className="space-y-3">
243
- {features.map((f, i) => (
244
- <div key={i} className="flex items-center gap-3">
245
- <div className="w-9 h-9 flex items-center justify-center rounded-lg bg-stone-200 dark:bg-stone-800 border border-stone-200 dark:border-stone-700 text-stone-500 dark:text-stone-400 shrink-0">
246
- <DynamicIcon
247
- className="dark:text-indigo-400 text-indigo-700 group-hover:dark:text-lime-400 duration-300 transition-all"
248
- name={f.iconName}
249
- size={14}
250
- />
251
- </div>
252
- <span className="text-xs text-stone-600 dark:text-stone-400">{f.text}</span>
253
- </div>
254
- ))}
255
- </div>
256
- </div>
257
- )}
258
-
259
- {/* Timezone note */}
260
- <div className="flex items-center gap-2.5 px-4 py-3 rounded-xl bg-stone-100/60 dark:bg-stone-900/40 border border-stone-200 dark:border-stone-800">
261
- <Globe size={14} className="text-stone-500 dark:text-stone-500 shrink-0" />
262
- <span className="text-xs text-stone-500 dark:text-stone-500">
263
- All times shown in{' '}
264
- <span className="text-stone-700 dark:text-stone-400 font-medium">
265
- {Intl.DateTimeFormat().resolvedOptions().timeZone}
266
- </span>
267
- </span>
268
- </div>
269
- </div>
270
-
271
- {/* Right — booking form */}
272
- <div className="lg:col-span-3">
273
- {status === 'success' ? (
274
- <div className="rounded-2xl bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-700 p-12 flex flex-col items-center justify-center text-center">
275
- <div className="relative mb-6">
276
- <div className="w-16 h-16 flex items-center justify-center rounded-full bg-lime-400/10 border border-emerald-400/30">
277
- <CalendarCheck size={28} className="text-emerald-400" />
278
- </div>
279
- <div className="absolute -top-1 -right-1 w-6 h-6 flex items-center justify-center rounded-full bg-lime-400 text-stone-50 dark:text-stone-900">
280
- <Check size={12} strokeWidth={3} />
281
- </div>
282
- </div>
283
- <h3 className="text-2xl font-bold text-stone-900 dark:text-stone-50 mb-2">
284
- You’re booked!
285
- </h3>
286
- <p className="text-stone-600 dark:text-stone-400 text-sm max-w-xs mb-2">
287
- A calendar invite will be in your inbox shortly. We’re looking forward to
288
- chatting.
289
- </p>
290
- {selectedCallType && selectedDate && selectedSlot && (
291
- <div className="flex items-center gap-2 mt-2 px-4 py-2.5 rounded-xl bg-stone-100 dark:bg-stone-800/60 border border-stone-200 dark:border-stone-700">
292
- <Clock size={14} className="text-emerald-400" />
293
- <span className="text-sm text-stone-700 dark:text-stone-300 font-medium">
294
- {selectedCallType.label} · {selectedDate} · {selectedSlot}
295
- </span>
296
- </div>
297
- )}
298
- <button
299
- onClick={() => setStatus('idle')}
300
- className="mt-8 px-5 py-2.5 rounded-lg bg-stone-200 dark:bg-stone-800 text-stone-800 dark:text-stone-200 text-sm font-medium hover:bg-stone-300 dark:hover:bg-stone-700 transition-colors cursor-pointer whitespace-nowrap"
301
- >
302
- Book another call
303
- </button>
304
- </div>
305
- ) : (
306
- <form
307
- id={formIdentifier}
308
- name={formIdentifier}
309
- data-form-id={String(formID || '')}
310
- onSubmit={handleSubmit}
311
- className="rounded-2xl bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 px-3 md:px-8 py-8 space-y-6"
312
- >
313
- {/* Name + Email */}
314
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
315
- <div>
316
- <label
317
- htmlFor={`${formIdentifier}-name`}
318
- className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
319
- >
320
- Your Name <span className="text-stone-400 dark:text-stone-600">*</span>
321
- </label>
322
- <input
323
- id={`${formIdentifier}-name`}
324
- type="text"
325
- name="name"
326
- autoComplete="name"
327
- required
328
- placeholder="Alex Rivera"
329
- className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 focus:ring-2 focus:ring-ring focus:ring-offset-2"
330
- />
331
- </div>
332
- <div>
333
- <label
334
- htmlFor={`${formIdentifier}-email`}
335
- className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
336
- >
337
- Email <span className="text-stone-400 dark:text-stone-600">*</span>
338
- </label>
339
- <input
340
- id={`${formIdentifier}-email`}
341
- type="email"
342
- name="email"
343
- autoComplete="email"
344
- required
345
- placeholder="you@company.com"
346
- className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 focus:ring-2 focus:ring-ring focus:ring-offset-2"
347
- />
348
- </div>
349
- </div>
350
-
351
- {/* Company */}
352
- <div>
353
- <label
354
- htmlFor={`${formIdentifier}-company`}
355
- className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
356
- >
357
- Company / Project
358
- </label>
359
- <input
360
- id={`${formIdentifier}-company`}
361
- type="text"
362
- name="company"
363
- autoComplete="organization"
364
- placeholder="Acme Inc."
365
- className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 focus:ring-2 focus:ring-ring focus:ring-offset-2"
366
- />
367
- </div>
368
-
369
- {/* Date + Time */}
370
- <div>
371
- <p className="text-xs font-medium text-stone-600 dark:text-stone-400 mb-3">
372
- Preferred Date &amp; Time{' '}
373
- <span className="text-stone-400 dark:text-stone-600">*</span>
374
- </p>
375
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
376
- {/* Date picker */}
377
- <div>
378
- <label
379
- htmlFor={`${formIdentifier}-date`}
380
- className="block text-xs text-stone-500 dark:text-stone-500 mb-1.5"
381
- >
382
- Date
383
- </label>
384
- <div className="relative">
385
- <div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-stone-400 dark:text-stone-500">
386
- <CalendarDays size={14} />
387
- </div>
388
- <input
389
- id={`${formIdentifier}-date`}
390
- type="date"
391
- name="preferred_date"
392
- required
393
- min={getMinDate()}
394
- max={getMaxDate()}
395
- value={selectedDate}
396
- onChange={(e) => setSelectedDate(e.target.value)}
397
- className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 pl-9 pr-3 py-2 text-sm text-stone-900 dark:text-stone-200 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 cursor-pointer"
398
- style={{ colorScheme: 'light dark' }}
399
- />
400
- </div>
401
- </div>
402
-
403
- {/* Timezone */}
404
- <div>
405
- <label className="block text-xs text-stone-500 dark:text-stone-500 mb-1.5">
406
- Time Zone
407
- </label>
408
- <div className="flex h-10 w-full items-center gap-2.5 rounded-2xl squircle border border-stone-300 bg-stone-50/80 px-3 py-2 text-sm text-stone-900 outline-none backdrop-blur-[22px] transition-all duration-300 dark:border-stone-600 dark:bg-[#0e0e13] dark:text-stone-200">
409
- <Globe
410
- size={14}
411
- className="text-stone-400 dark:text-stone-500 shrink-0"
412
- />
413
- <span className="truncate text-sm text-stone-900 dark:text-stone-200">
414
- {Intl.DateTimeFormat().resolvedOptions().timeZone}
415
- </span>
416
- </div>
417
- </div>
418
- </div>
419
-
420
- {/* Time slot grid */}
421
- {slots && slots.length > 0 && (
422
- <div className="mt-4">
423
- <label className="block text-xs text-stone-500 dark:text-stone-500 mb-2.5">
424
- Available Times
425
- </label>
426
- <div className="grid grid-cols-4 sm:grid-cols-5 gap-2">
427
- {slots.map((slot) => (
428
- <button
429
- key={slot}
430
- type="button"
431
- onClick={() => setSelectedSlot(slot)}
432
- className={`py-2 rounded-lg text-xs font-medium border transition-all cursor-pointer whitespace-nowrap ${
433
- selectedSlot === slot
434
- ? 'bg-stone-900 dark:!bg-lime-950/50 text-stone-50 dark:text-lime-500 border-stone-900 dark:border-lime-500'
435
- : 'bg-stone-100 dark:bg-stone-950 border-stone-200 dark:border-stone-700 text-stone-600 dark:text-stone-400 hover:border-stone-300 dark:hover:border-stone-500 hover:text-stone-900 dark:hover:text-stone-200'
436
- }`}
437
- >
438
- {slot}
439
- </button>
440
- ))}
441
- </div>
442
- {!selectedSlot && (
443
- <p className="text-xs text-stone-700 dark:text-stone-400 mt-2">
444
- Select a time slot above
445
- </p>
446
- )}
447
- {selectedSlot && selectedCallType && (
448
- <div className="mt-2 flex items-center gap-2">
449
- <span className="w-1.5 h-1.5 rounded-full bg-lime-400 shrink-0" />
450
- <p className="text-xs text-emerald-400 font-medium">
451
- {selectedSlot} selected · {selectedCallType.duration} session
452
- </p>
453
- </div>
454
- )}
455
- </div>
456
- )}
457
- </div>
458
-
459
- {/* Notes */}
460
- <div>
461
- <label
462
- htmlFor={`${formIdentifier}-notes`}
463
- className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
464
- >
465
- Notes{' '}
466
- <span
467
- className={`font-normal ml-1 ${
468
- charCount > 500 ? 'text-red-400' : 'text-stone-400 dark:text-stone-600'
469
- }`}
470
- >
471
- {charCount}/500
472
- </span>
473
- </label>
474
- <textarea
475
- id={`${formIdentifier}-notes`}
476
- name="notes"
477
- rows={3}
478
- maxLength={500}
479
- placeholder="Anything you'd like us to know before the call — project overview, specific questions, technical context..."
480
- onChange={(e) => setCharCount(e.target.value.length)}
481
- className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 resize-none"
482
- />
483
- </div>
484
-
485
- {/* Error */}
486
- {status === 'error' && (
487
- <p className="text-xs text-red-400 flex items-center gap-1.5">
488
- <AlertCircle size={13} />
489
- Something went wrong. Please try again.
490
- </p>
491
- )}
492
-
493
- {/* Submit */}
494
- <button
495
- type="submit"
496
- disabled={
497
- status === 'submitting' || !selectedSlot || !selectedDate || charCount > 500
498
- }
499
- className="group relative isolate flex w-full cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-4xl! border border-stone-300 dark:border-white/30 bg-stone-50/90 dark:bg-[linear-gradient(180deg,rgba(255,255,255,.95),rgba(255,255,255,.68)_24%,rgba(255,255,255,.7)_58%,rgba(255,255,255,.7)),linear-gradient(145deg,rgba(255,255,255,.75),rgba(255,255,255,.8)_38%,rgba(0,0,0,.12)),rgba(255,255,255,.4)] px-6 py-3 text-sm font-semibold text-stone-900 dark:text-stone-900 disabled:opacity-40 disabled:cursor-not-allowed"
500
- >
501
- <span className="relative z-2 flex items-center gap-2">
502
- {status === 'submitting' ? (
503
- <>
504
- <Loader2 size={15} className="animate-spin" />
505
- Booking your call...
506
- </>
507
- ) : (
508
- <>
509
- <CalendarDays size={15} />
510
- Confirm Booking
511
- {selectedSlot && selectedDate && (
512
- <span className="text-stone-500 dark:text-stone-500 font-normal text-xs ml-1">
513
- · {selectedDate}, {selectedSlot}
514
- </span>
515
- )}
516
- </>
517
- )}
518
- </span>
519
- </button>
520
-
521
- {footerNote && (
522
- <p className="text-xs text-stone-700 dark:text-stone-400 text-center">
523
- {footerNote}
524
- </p>
525
- )}
526
- </form>
527
- )}
528
- </div>
529
- </div>
530
- </div>
531
- </section>
532
- </>
533
- )
534
- }
1
+ 'use client'
2
+ import React, { type FormEvent, useMemo, useState } from 'react'
3
+ import * as LucideIcons from 'lucide-react'
4
+ import {
5
+ Check,
6
+ Loader2,
7
+ CalendarDays,
8
+ Globe,
9
+ Clock,
10
+ AlertCircle,
11
+ CalendarCheck,
12
+ } from 'lucide-react'
13
+ import { Input } from '@/components/ui/input'
14
+ import { getClientSideURL } from '@/utilities/getURL'
15
+
16
+ type Status = 'idle' | 'submitting' | 'success' | 'error'
17
+ export type DDBookCallProps = {
18
+ eyebrow?: string | null
19
+ headlineMain?: string | null
20
+ headlineAccent?: string | null
21
+ description?: string | null
22
+ callTypes?: Array<{
23
+ value: string
24
+ label: string
25
+ duration: string
26
+ iconName?: string | null
27
+ desc?: string | null
28
+ }> | null
29
+ features?: Array<{
30
+ iconName?: string | null
31
+ text: string
32
+ }> | null
33
+ form?: string | number | { id?: string | number } | null
34
+ timeSlots?: Array<{ slot?: string | null }> | null
35
+ footerNote?: string | null
36
+ sectionId?: string | null
37
+ customCSS?: string | null
38
+ }
39
+ function DynamicIcon({
40
+ name,
41
+ size = 16,
42
+ ...props
43
+ }: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
44
+ if (!name) return null
45
+ const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
46
+ if (!Icon) return null
47
+ return <Icon size={size} {...props} />
48
+ }
49
+
50
+ function getMinDate() {
51
+ const d = new Date()
52
+ d.setDate(d.getDate() + 1)
53
+ return d.toISOString().split('T')[0]
54
+ }
55
+
56
+ function getMaxDate() {
57
+ const d = new Date()
58
+ d.setDate(d.getDate() + 60)
59
+ return d.toISOString().split('T')[0]
60
+ }
61
+
62
+ export const DDBookCall: React.FC<DDBookCallProps> = ({
63
+ eyebrow,
64
+ headlineMain,
65
+ headlineAccent,
66
+ description,
67
+ callTypes,
68
+ features,
69
+ form,
70
+ timeSlots,
71
+ footerNote,
72
+ sectionId,
73
+ customCSS,
74
+ }) => {
75
+ const [status, setStatus] = useState<Status>('idle')
76
+ const [selectedType, setSelectedType] = useState<string>(callTypes?.[0]?.value ?? 'discovery')
77
+ const [selectedSlot, setSelectedSlot] = useState('')
78
+ const [selectedDate, setSelectedDate] = useState('')
79
+ const [charCount, setCharCount] = useState(0)
80
+
81
+ const formID = typeof form === 'object' && form !== null ? form.id : form
82
+
83
+ const formIdentifier = useMemo(() => {
84
+ const safeSection = (sectionId || 'book-call').toLowerCase().replace(/[^a-z0-9-_]/g, '-')
85
+ return `studio-book-call-${String(formID || 'unknown')}-${safeSection}`
86
+ }, [formID, sectionId])
87
+
88
+ const selectedCallType = callTypes?.find((t) => t.value === selectedType) ?? callTypes?.[0]
89
+ const slots = timeSlots?.map((t) => t.slot).filter(Boolean) as string[]
90
+
91
+ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
92
+ e.preventDefault()
93
+ const formElement = e.currentTarget
94
+
95
+ if (!selectedDate || !selectedSlot) return
96
+
97
+ const notes = formElement.querySelector<HTMLTextAreaElement>('[name="notes"]')?.value ?? ''
98
+ if (notes.length > 500) return
99
+
100
+ setStatus('submitting')
101
+
102
+ const data = new FormData(formElement)
103
+
104
+ const submissionData = [
105
+ { field: 'name', value: data.get('name') },
106
+ { field: 'email', value: data.get('email') },
107
+ { field: 'company', value: data.get('company') },
108
+ { field: 'call_type', value: selectedType },
109
+ { field: 'preferred_date', value: selectedDate },
110
+ { field: 'preferred_time', value: selectedSlot },
111
+ { field: 'notes', value: data.get('notes') },
112
+ ]
113
+
114
+ try {
115
+ const res = await fetch(`${getClientSideURL()}/api/form-submissions`, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({ form: formID, submissionData }),
119
+ })
120
+
121
+ if (res.ok) {
122
+ setStatus('success')
123
+ formElement.reset()
124
+ setSelectedSlot('')
125
+ setSelectedDate('')
126
+ setCharCount(0)
127
+ } else {
128
+ setStatus('error')
129
+ }
130
+ } catch {
131
+ setStatus('error')
132
+ }
133
+ }
134
+
135
+ return (
136
+ <>
137
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
138
+
139
+ <section
140
+ id={sectionId ?? 'book-call'}
141
+ className="py-28 bg-stone-50 dark:bg-stone-950 relative overflow-hidden"
142
+ >
143
+ {/* Background gradients – light & dark variants */}
144
+ <div
145
+ className="absolute inset-0 pointer-events-none opacity-30
146
+ bg-[radial-gradient(circle_at_20%_50%,rgba(0,0,0,0.06)_0%,transparent_50%),
147
+ radial-gradient(circle_at_80%_20%,rgba(0,0,0,0.03)_0%,transparent_40%)]
148
+ dark:bg-[radial-gradient(circle_at_20%_50%,rgba(110,231,183,0.06)_0%,transparent_50%),
149
+ radial-gradient(circle_at_80%_20%,rgba(255,255,255,0.03)_0%,transparent_40%)]"
150
+ />
151
+ <div
152
+ className="absolute inset-0 pointer-events-none
153
+ bg-[linear-gradient(rgba(0,0,0,0.06)_1px,transparent_1px),
154
+ linear-gradient(90deg,rgba(0,0,0,0.06)_1px,transparent_1px)]
155
+ dark:bg-[linear-gradient(rgba(255,255,255,0.012)_1px,transparent_1px),
156
+ linear-gradient(90deg,rgba(255,255,255,0.012)_1px,transparent_1px)]
157
+ bg-size-[60px_60px]"
158
+ />
159
+
160
+ <div className="relative max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
161
+ {/* Header */}#BlockName-DDBookCall
162
+ <div className="text-center mb-16">
163
+ {eyebrow && (
164
+ <div className="inline-flex items-center gap-2 mb-4">
165
+ <span className="w-1.5 h-1.5 rounded-full bg-lime-400 animate-pulse" />
166
+ <span className="text-xs font-semibold tracking-widest uppercase text-stone-500 dark:text-stone-500">
167
+ {eyebrow}
168
+ </span>
169
+ </div>
170
+ )}
171
+ <h2 className="text-4xl md:text-5xl font-bold text-stone-900 dark:text-stone-50 leading-tight mb-4">
172
+ {headlineMain}
173
+ <br />
174
+ <span className="text-stone-600 dark:text-stone-400">{headlineAccent}</span>
175
+ </h2>
176
+ {description && (
177
+ <p className="text-stone-600 dark:text-stone-500 text-base max-w-md mx-auto">
178
+ {description}
179
+ </p>
180
+ )}
181
+ </div>
182
+ <div className="grid grid-cols-1 lg:grid-cols-5 gap-8 items-start">
183
+ {/* Left sidebar */}
184
+ <div className="lg:col-span-2 space-y-6">
185
+ {/* Call type selector */}
186
+ {callTypes && callTypes.length > 0 && (
187
+ <div>
188
+ <p className="text-xs font-semibold text-stone-500 dark:text-stone-500 uppercase tracking-widest mb-3">
189
+ Select Session Type
190
+ </p>
191
+ <div className="space-y-2.5">
192
+ {callTypes.map((type) => (
193
+ <button
194
+ key={type.value}
195
+ type="button"
196
+ onClick={() => setSelectedType(type.value)}
197
+ className={`w-full flex items-center gap-4 p-4 rounded-xl border transition-all cursor-pointer text-left ${
198
+ selectedType === type.value
199
+ ? 'bg-stone-50/80 dark:bg-stone-50/[0.08] border-stone-300 dark:border-white/20 text-stone-900 dark:text-stone-50'
200
+ : 'bg-stone-100/60 dark:bg-stone-900/50 border-stone-200 dark:border-stone-800 text-stone-600 dark:text-stone-400 hover:border-stone-300 dark:hover:border-stone-700 hover:text-stone-900 dark:hover:text-stone-300'
201
+ }`}
202
+ >
203
+ <div
204
+ className={`w-9 h-9 flex items-center justify-center rounded-lg shrink-0 transition-colors ${
205
+ selectedType === type.value
206
+ ? 'bg-lime-400/15 text-emerald-400'
207
+ : 'bg-stone-200 dark:bg-stone-800 text-stone-500 dark:text-stone-500'
208
+ }`}
209
+ >
210
+ <DynamicIcon
211
+ className="transition-colors duration-300"
212
+ name={type.iconName}
213
+ size={15}
214
+ />
215
+ </div>
216
+ <div className="flex-1 min-w-0">
217
+ <div className="flex items-center justify-between gap-2">
218
+ <span className="text-sm font-semibold">{type.label}</span>
219
+ <span
220
+ className={`text-xs font-medium px-2 py-0.5 rounded-full whitespace-nowrap ${
221
+ selectedType === type.value
222
+ ? 'bg-lime-400/15 text-emerald-400'
223
+ : 'bg-stone-200 dark:bg-stone-800 text-stone-500 dark:text-stone-500'
224
+ }`}
225
+ >
226
+ {type.duration}
227
+ </span>
228
+ </div>
229
+ {type.desc && <p className="text-xs mt-0.5 opacity-70">{type.desc}</p>}
230
+ </div>
231
+ </button>
232
+ ))}
233
+ </div>
234
+ </div>
235
+ )}
236
+
237
+ {/* What to expect */}
238
+ {features && features.length > 0 && (
239
+ <div className="rounded-xl bg-stone-100/60 dark:bg-stone-900/50 border border-stone-200 dark:border-stone-800 p-5">
240
+ <p className="text-xs font-semibold text-stone-500 dark:text-stone-500 uppercase tracking-widest mb-4">
241
+ What to expect
242
+ </p>
243
+ <div className="space-y-3">
244
+ {features.map((f, i) => (
245
+ <div key={i} className="flex items-center gap-3">
246
+ <div className="w-9 h-9 flex items-center justify-center rounded-lg bg-stone-200 dark:bg-stone-800 border border-stone-200 dark:border-stone-700 text-stone-500 dark:text-stone-400 shrink-0">
247
+ <DynamicIcon
248
+ className="dark:text-indigo-400 text-indigo-700 group-hover:dark:text-lime-400 duration-300 transition-all"
249
+ name={f.iconName}
250
+ size={14}
251
+ />
252
+ </div>
253
+ <span className="text-xs text-stone-600 dark:text-stone-400">{f.text}</span>
254
+ </div>
255
+ ))}
256
+ </div>
257
+ </div>
258
+ )}
259
+
260
+ {/* Timezone note */}
261
+ <div className="flex items-center gap-2.5 px-4 py-3 rounded-xl bg-stone-100/60 dark:bg-stone-900/40 border border-stone-200 dark:border-stone-800">
262
+ <Globe size={14} className="text-stone-500 dark:text-stone-500 shrink-0" />
263
+ <span className="text-xs text-stone-500 dark:text-stone-500">
264
+ All times shown in{' '}
265
+ <span className="text-stone-700 dark:text-stone-400 font-medium">
266
+ {Intl.DateTimeFormat().resolvedOptions().timeZone}
267
+ </span>
268
+ </span>
269
+ </div>
270
+ </div>
271
+
272
+ {/* Right — booking form */}
273
+ <div className="lg:col-span-3">
274
+ {status === 'success' ? (
275
+ <div className="rounded-2xl bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-700 p-12 flex flex-col items-center justify-center text-center">
276
+ <div className="relative mb-6">
277
+ <div className="w-16 h-16 flex items-center justify-center rounded-full bg-lime-400/10 border border-emerald-400/30">
278
+ <CalendarCheck size={28} className="text-emerald-400" />
279
+ </div>
280
+ <div className="absolute -top-1 -right-1 w-6 h-6 flex items-center justify-center rounded-full bg-lime-400 text-stone-50 dark:text-stone-900">
281
+ <Check size={12} strokeWidth={3} />
282
+ </div>
283
+ </div>
284
+ <h3 className="text-2xl font-bold text-stone-900 dark:text-stone-50 mb-2">
285
+ You’re booked!
286
+ </h3>
287
+ <p className="text-stone-600 dark:text-stone-400 text-sm max-w-xs mb-2">
288
+ A calendar invite will be in your inbox shortly. We’re looking forward to
289
+ chatting.
290
+ </p>
291
+ {selectedCallType && selectedDate && selectedSlot && (
292
+ <div className="flex items-center gap-2 mt-2 px-4 py-2.5 rounded-xl bg-stone-100 dark:bg-stone-800/60 border border-stone-200 dark:border-stone-700">
293
+ <Clock size={14} className="text-emerald-400" />
294
+ <span className="text-sm text-stone-700 dark:text-stone-300 font-medium">
295
+ {selectedCallType.label} · {selectedDate} · {selectedSlot}
296
+ </span>
297
+ </div>
298
+ )}
299
+ <button
300
+ onClick={() => setStatus('idle')}
301
+ className="mt-8 px-5 py-2.5 rounded-lg bg-stone-200 dark:bg-stone-800 text-stone-800 dark:text-stone-200 text-sm font-medium hover:bg-stone-300 dark:hover:bg-stone-700 transition-colors cursor-pointer whitespace-nowrap"
302
+ >
303
+ Book another call
304
+ </button>
305
+ </div>
306
+ ) : (
307
+ <form
308
+ id={formIdentifier}
309
+ name={formIdentifier}
310
+ data-form-id={String(formID || '')}
311
+ onSubmit={handleSubmit}
312
+ className="rounded-2xl bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 px-3 md:px-8 py-8 space-y-6"
313
+ >
314
+ {/* Name + Email */}
315
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
316
+ <div>
317
+ <label
318
+ htmlFor={`${formIdentifier}-name`}
319
+ className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
320
+ >
321
+ Your Name <span className="text-stone-400 dark:text-stone-600">*</span>
322
+ </label>
323
+ <input
324
+ id={`${formIdentifier}-name`}
325
+ type="text"
326
+ name="name"
327
+ autoComplete="name"
328
+ required
329
+ placeholder="Alex Rivera"
330
+ className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 focus:ring-2 focus:ring-ring focus:ring-offset-2"
331
+ />
332
+ </div>
333
+ <div>
334
+ <label
335
+ htmlFor={`${formIdentifier}-email`}
336
+ className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
337
+ >
338
+ Email <span className="text-stone-400 dark:text-stone-600">*</span>
339
+ </label>
340
+ <input
341
+ id={`${formIdentifier}-email`}
342
+ type="email"
343
+ name="email"
344
+ autoComplete="email"
345
+ required
346
+ placeholder="you@company.com"
347
+ className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 focus:ring-2 focus:ring-ring focus:ring-offset-2"
348
+ />
349
+ </div>
350
+ </div>
351
+
352
+ {/* Company */}
353
+ <div>
354
+ <label
355
+ htmlFor={`${formIdentifier}-company`}
356
+ className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
357
+ >
358
+ Company / Project
359
+ </label>
360
+ <input
361
+ id={`${formIdentifier}-company`}
362
+ type="text"
363
+ name="company"
364
+ autoComplete="organization"
365
+ placeholder="Acme Inc."
366
+ className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 focus:ring-2 focus:ring-ring focus:ring-offset-2"
367
+ />
368
+ </div>
369
+
370
+ {/* Date + Time */}
371
+ <div>
372
+ <p className="text-xs font-medium text-stone-600 dark:text-stone-400 mb-3">
373
+ Preferred Date &amp; Time{' '}
374
+ <span className="text-stone-400 dark:text-stone-600">*</span>
375
+ </p>
376
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
377
+ {/* Date picker */}
378
+ <div>
379
+ <label
380
+ htmlFor={`${formIdentifier}-date`}
381
+ className="block text-xs text-stone-500 dark:text-stone-500 mb-1.5"
382
+ >
383
+ Date
384
+ </label>
385
+ <div className="relative">
386
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-stone-400 dark:text-stone-500">
387
+ <CalendarDays size={14} />
388
+ </div>
389
+ <input
390
+ id={`${formIdentifier}-date`}
391
+ type="date"
392
+ name="preferred_date"
393
+ required
394
+ min={getMinDate()}
395
+ max={getMaxDate()}
396
+ value={selectedDate}
397
+ onChange={(e) => setSelectedDate(e.target.value)}
398
+ className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 pl-9 pr-3 py-2 text-sm text-stone-900 dark:text-stone-200 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 cursor-pointer"
399
+ style={{ colorScheme: 'light dark' }}
400
+ />
401
+ </div>
402
+ </div>
403
+
404
+ {/* Timezone */}
405
+ <div>
406
+ <label className="block text-xs text-stone-500 dark:text-stone-500 mb-1.5">
407
+ Time Zone
408
+ </label>
409
+ <div className="flex items-center gap-2.5 w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 backdrop-blur-[22px]">
410
+ <Globe
411
+ size={14}
412
+ className="text-stone-400 dark:text-stone-500 shrink-0"
413
+ />
414
+ <span className="text-sm text-stone-700 dark:text-stone-400 truncate">
415
+ {Intl.DateTimeFormat().resolvedOptions().timeZone}
416
+ </span>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ {/* Time slot grid */}
422
+ {slots && slots.length > 0 && (
423
+ <div className="mt-4">
424
+ <label className="block text-xs text-stone-500 dark:text-stone-500 mb-2.5">
425
+ Available Times
426
+ </label>
427
+ <div className="grid grid-cols-4 sm:grid-cols-5 gap-2">
428
+ {slots.map((slot) => (
429
+ <button
430
+ key={slot}
431
+ type="button"
432
+ onClick={() => setSelectedSlot(slot)}
433
+ className={`py-2 rounded-lg text-xs font-medium border transition-all cursor-pointer whitespace-nowrap ${
434
+ selectedSlot === slot
435
+ ? 'bg-stone-900 dark:bg-stone-50 text-stone-50 dark:text-stone-900 border-stone-900 dark:border-white'
436
+ : 'bg-stone-100 dark:bg-stone-950 border-stone-200 dark:border-stone-700 text-stone-600 dark:text-stone-400 hover:border-stone-300 dark:hover:border-stone-500 hover:text-stone-900 dark:hover:text-stone-200'
437
+ }`}
438
+ >
439
+ {slot}
440
+ </button>
441
+ ))}
442
+ </div>
443
+ {!selectedSlot && (
444
+ <p className="text-xs text-stone-700 dark:text-stone-400 mt-2">
445
+ Select a time slot above
446
+ </p>
447
+ )}
448
+ {selectedSlot && selectedCallType && (
449
+ <div className="mt-2 flex items-center gap-2">
450
+ <span className="w-1.5 h-1.5 rounded-full bg-lime-400 shrink-0" />
451
+ <p className="text-xs text-emerald-400 font-medium">
452
+ {selectedSlot} selected · {selectedCallType.duration} session
453
+ </p>
454
+ </div>
455
+ )}
456
+ </div>
457
+ )}
458
+ </div>
459
+
460
+ {/* Notes */}
461
+ <div>
462
+ <label
463
+ htmlFor={`${formIdentifier}-notes`}
464
+ className="block text-xs font-medium text-stone-600 dark:text-stone-400 mb-1.5"
465
+ >
466
+ Notes{' '}
467
+ <span
468
+ className={`font-normal ml-1 ${
469
+ charCount > 500 ? 'text-red-400' : 'text-stone-400 dark:text-stone-600'
470
+ }`}
471
+ >
472
+ {charCount}/500
473
+ </span>
474
+ </label>
475
+ <textarea
476
+ id={`${formIdentifier}-notes`}
477
+ name="notes"
478
+ rows={3}
479
+ maxLength={500}
480
+ placeholder="Anything you'd like us to know before the call — project overview, specific questions, technical context..."
481
+ onChange={(e) => setCharCount(e.target.value.length)}
482
+ className="w-full rounded-sm border border-stone-300 dark:border-stone-600 bg-stone-50/80 dark:bg-black/80 px-3 py-2 text-sm text-stone-900 dark:text-stone-200 placeholder:text-stone-400 dark:placeholder:text-stone-500 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-stone-500 resize-none"
483
+ />
484
+ </div>
485
+
486
+ {/* Error */}
487
+ {status === 'error' && (
488
+ <p className="text-xs text-red-400 flex items-center gap-1.5">
489
+ <AlertCircle size={13} />
490
+ Something went wrong. Please try again.
491
+ </p>
492
+ )}
493
+
494
+ {/* Submit */}
495
+ <button
496
+ type="submit"
497
+ disabled={
498
+ status === 'submitting' || !selectedSlot || !selectedDate || charCount > 500
499
+ }
500
+ className="group relative isolate flex w-full cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-4xl! border border-stone-300 dark:border-white/30 bg-stone-50/90 dark:bg-[linear-gradient(180deg,rgba(255,255,255,.95),rgba(255,255,255,.68)_24%,rgba(255,255,255,.7)_58%,rgba(255,255,255,.7)),linear-gradient(145deg,rgba(255,255,255,.75),rgba(255,255,255,.8)_38%,rgba(0,0,0,.12)),rgba(255,255,255,.4)] px-6 py-3 text-sm font-semibold text-stone-900 dark:text-stone-900 disabled:opacity-40 disabled:cursor-not-allowed"
501
+ >
502
+ <span className="relative z-2 flex items-center gap-2">
503
+ {status === 'submitting' ? (
504
+ <>
505
+ <Loader2 size={15} className="animate-spin" />
506
+ Booking your call...
507
+ </>
508
+ ) : (
509
+ <>
510
+ <CalendarDays size={15} />
511
+ Confirm Booking
512
+ {selectedSlot && selectedDate && (
513
+ <span className="text-stone-500 dark:text-stone-500 font-normal text-xs ml-1">
514
+ · {selectedDate}, {selectedSlot}
515
+ </span>
516
+ )}
517
+ </>
518
+ )}
519
+ </span>
520
+ </button>
521
+
522
+ {footerNote && (
523
+ <p className="text-xs text-stone-700 dark:text-stone-400 text-center">
524
+ {footerNote}
525
+ </p>
526
+ )}
527
+ </form>
528
+ )}
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </section>
533
+ </>
534
+ )
535
+ }