blocks-dusted 0.1.10 → 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 (249) hide show
  1. package/README.md +35 -0
  2. package/package.json +5 -2
  3. package/src/cli.js +39 -3
  4. package/src/commands/shop.js +59 -0
  5. package/src/commands/warehouse.js +73 -0
  6. package/src/utils/paths.js +13 -13
  7. package/templates/blocks/DD-Accordion/Component.tsx +229 -0
  8. package/templates/blocks/DD-Accordion/README.md +3 -0
  9. package/templates/blocks/DD-Accordion/config.ts +73 -0
  10. package/templates/blocks/DD-Accordion/manifest.json +75 -0
  11. package/templates/blocks/DD-AnimText/Component.tsx +788 -0
  12. package/templates/blocks/DD-AnimText/README.md +3 -0
  13. package/templates/blocks/DD-AnimText/config.ts +295 -0
  14. package/templates/blocks/DD-AnimText/manifest.json +85 -0
  15. package/templates/blocks/DD-AsideNav/Component.tsx +321 -0
  16. package/templates/blocks/DD-AsideNav/config.ts +204 -0
  17. package/templates/blocks/DD-AsideNav/manifest.json +80 -0
  18. package/templates/blocks/DD-Banner/Component.tsx +51 -0
  19. package/templates/blocks/DD-Banner/config.ts +57 -0
  20. package/templates/blocks/DD-Banner/manifest.json +72 -0
  21. package/templates/blocks/DD-BgLightning/Component.tsx +44 -0
  22. package/templates/blocks/DD-BgLightning/README.md +3 -0
  23. package/templates/blocks/DD-BgLightning/config.ts +97 -0
  24. package/templates/blocks/DD-BgLightning/manifest.json +79 -0
  25. package/templates/blocks/DD-BookCall/Component.tsx +535 -535
  26. package/templates/blocks/DD-BookCall/config.ts +238 -238
  27. package/templates/blocks/DD-ButtonPop/Component.tsx +117 -0
  28. package/templates/blocks/DD-ButtonPop/config.ts +105 -0
  29. package/templates/blocks/DD-ButtonPop/manifest.json +88 -0
  30. package/templates/blocks/DD-ButtonPop/svg-icons/Dust2.tsx +167 -0
  31. package/templates/blocks/DD-Card/Component.tsx +251 -0
  32. package/templates/blocks/DD-Card/README.md +4 -0
  33. package/templates/blocks/DD-Card/config.ts +154 -0
  34. package/templates/blocks/DD-Card/manifest.json +82 -0
  35. package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -140
  36. package/templates/blocks/DD-CardTemplate01/config.ts +106 -106
  37. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -249
  38. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -403
  39. package/templates/blocks/DD-Chip/Component.tsx +250 -0
  40. package/templates/blocks/DD-Chip/EXAMPLES.md +313 -0
  41. package/templates/blocks/DD-Chip/README.md +202 -0
  42. package/templates/blocks/DD-Chip/config.ts +369 -0
  43. package/templates/blocks/DD-Chip/manifest.json +74 -0
  44. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -256
  45. package/templates/blocks/DD-Contact/Component.tsx +435 -434
  46. package/templates/blocks/DD-Contact/config.ts +22 -22
  47. package/templates/blocks/DD-Counter/Component.tsx +190 -0
  48. package/templates/blocks/DD-Counter/README.md +4 -0
  49. package/templates/blocks/DD-Counter/config.ts +291 -0
  50. package/templates/blocks/DD-Counter/manifest.json +74 -0
  51. package/templates/blocks/DD-CustomCSS/Component.tsx +8 -0
  52. package/templates/blocks/DD-CustomCSS/README.md +4 -0
  53. package/templates/blocks/DD-CustomCSS/addToLayout.md +5 -0
  54. package/templates/blocks/DD-CustomCSS/config.ts +22 -0
  55. package/templates/blocks/DD-CustomCSS/manifest.json +72 -0
  56. package/templates/blocks/DD-CustomCSS/renderCustomCSS.md +7 -0
  57. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  58. package/templates/blocks/DD-FeatCat/config.ts +201 -201
  59. package/templates/blocks/DD-FeatStrip/Component.tsx +585 -585
  60. package/templates/blocks/DD-FeatStrip/config.ts +134 -134
  61. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +628 -0
  62. package/templates/blocks/DD-FluidBtn/Component.tsx +43 -0
  63. package/templates/blocks/DD-FluidBtn/README.md +6 -0
  64. package/templates/blocks/DD-FluidBtn/config.ts +102 -0
  65. package/templates/blocks/DD-FluidBtn/manifest.json +86 -0
  66. package/templates/blocks/DD-GooeyInfo/Component.tsx +149 -0
  67. package/templates/blocks/DD-GooeyInfo/config.ts +135 -0
  68. package/templates/blocks/DD-GooeyInfo/manifest.json +72 -0
  69. package/templates/blocks/DD-Hero/Component.tsx +297 -297
  70. package/templates/blocks/DD-Hero/config.ts +267 -267
  71. package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -936
  72. package/templates/blocks/DD-HorizontalScroll/config.ts +231 -231
  73. package/templates/blocks/DD-Icon/Component.tsx +287 -0
  74. package/templates/blocks/DD-Icon/README.md +4 -0
  75. package/templates/blocks/DD-Icon/config.ts +258 -0
  76. package/templates/blocks/DD-Icon/hooks/revalidateBlockIds.ts +66 -0
  77. package/templates/blocks/DD-Icon/manifest.json +82 -0
  78. package/templates/blocks/DD-IconButton/Component.tsx +455 -0
  79. package/templates/blocks/DD-IconButton/README.md +326 -0
  80. package/templates/blocks/DD-IconButton/config.ts +239 -0
  81. package/templates/blocks/DD-IconButton/manifest.json +85 -0
  82. package/templates/blocks/DD-IdTag/Component.tsx +21 -0
  83. package/templates/blocks/DD-IdTag/README.md +4 -0
  84. package/templates/blocks/DD-IdTag/config.ts +41 -0
  85. package/templates/blocks/DD-IdTag/manifest.json +72 -0
  86. package/templates/blocks/DD-Iframe/Component.tsx +45 -0
  87. package/templates/blocks/DD-Iframe/README.md +4 -0
  88. package/templates/blocks/DD-Iframe/config.ts +47 -0
  89. package/templates/blocks/DD-Iframe/manifest.json +72 -0
  90. package/templates/blocks/DD-IframeMedia/Component.tsx +133 -0
  91. package/templates/blocks/DD-IframeMedia/README.md +4 -0
  92. package/templates/blocks/DD-IframeMedia/config.ts +79 -0
  93. package/templates/blocks/DD-IframeMedia/manifest.json +72 -0
  94. package/templates/blocks/DD-LineAnim/Component.tsx +36 -0
  95. package/templates/blocks/DD-LineAnim/README.md +4 -0
  96. package/templates/blocks/DD-LineAnim/config.ts +90 -0
  97. package/templates/blocks/DD-LineAnim/manifest.json +80 -0
  98. package/templates/blocks/DD-LinkGrid/Component.tsx +119 -0
  99. package/templates/blocks/DD-LinkGrid/README.md +4 -0
  100. package/templates/blocks/DD-LinkGrid/config.ts +51 -0
  101. package/templates/blocks/DD-LinkGrid/manifest.json +74 -0
  102. package/templates/blocks/DD-LogoMarq/Component.tsx +132 -0
  103. package/templates/blocks/DD-LogoMarq/README.md +4 -0
  104. package/templates/blocks/DD-LogoMarq/config.ts +264 -0
  105. package/templates/blocks/DD-LogoMarq/manifest.json +82 -0
  106. package/templates/blocks/DD-Lottie/Component.tsx +459 -0
  107. package/templates/blocks/DD-Lottie/README.md +4 -0
  108. package/templates/blocks/DD-Lottie/config.ts +379 -0
  109. package/templates/blocks/DD-Lottie/index.tsx +28 -0
  110. package/templates/blocks/DD-Lottie/manifest.json +83 -0
  111. package/templates/blocks/DD-MagicBento/Component.tsx +777 -0
  112. package/templates/blocks/DD-MagicBento/README.md +4 -0
  113. package/templates/blocks/DD-MagicBento/config.ts +264 -0
  114. package/templates/blocks/DD-MagicBento/manifest.json +74 -0
  115. package/templates/blocks/DD-MasonaryMedia/Component.tsx +545 -545
  116. package/templates/blocks/DD-MasonaryMedia/config.ts +2 -2
  117. package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -2
  118. package/templates/blocks/DD-Masonry/Component.tsx +645 -0
  119. package/templates/blocks/DD-Masonry/README.md +4 -0
  120. package/templates/blocks/DD-Masonry/config.ts +475 -0
  121. package/templates/blocks/DD-Masonry/manifest.json +82 -0
  122. package/templates/blocks/DD-Pricing/Component.tsx +2 -5
  123. package/templates/blocks/DD-Process/Component.tsx +2 -4
  124. package/templates/blocks/DD-Process/config.ts +150 -150
  125. package/templates/blocks/DD-Progress/Component.tsx +156 -0
  126. package/templates/blocks/DD-Progress/README.md +4 -0
  127. package/templates/blocks/DD-Progress/config.ts +175 -0
  128. package/templates/blocks/DD-Progress/manifest.json +74 -0
  129. package/templates/blocks/DD-ProjHeader/Component.tsx +91 -0
  130. package/templates/blocks/DD-ProjHeader/README.md +4 -0
  131. package/templates/blocks/DD-ProjHeader/config.ts +96 -0
  132. package/templates/blocks/DD-ProjHeader/manifest.json +72 -0
  133. package/templates/blocks/DD-ProjSlider/Component.tsx +123 -0
  134. package/templates/blocks/DD-ProjSlider/README.md +4 -0
  135. package/templates/blocks/DD-ProjSlider/config.ts +64 -0
  136. package/templates/blocks/DD-ProjSlider/manifest.json +72 -0
  137. package/templates/blocks/DD-ScrollText/Component.tsx +114 -0
  138. package/templates/blocks/DD-ScrollText/README.md +4 -0
  139. package/templates/blocks/DD-ScrollText/config.ts +58 -0
  140. package/templates/blocks/DD-ScrollText/manifest.json +72 -0
  141. package/templates/blocks/DD-Section/Component.tsx +266 -266
  142. package/templates/blocks/DD-Separator/Component.tsx +66 -0
  143. package/templates/blocks/DD-Separator/README.md +4 -0
  144. package/templates/blocks/DD-Separator/config.ts +58 -0
  145. package/templates/blocks/DD-Separator/manifest.json +72 -0
  146. package/templates/blocks/DD-Services/Component.tsx +176 -176
  147. package/templates/blocks/DD-Services/config.ts +149 -149
  148. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -307
  149. package/templates/blocks/DD-Slider-A/Component.tsx +1 -1
  150. package/templates/blocks/DD-StackCards/Component.tsx +1 -1
  151. package/templates/blocks/DD-StackCards/config.ts +399 -399
  152. package/templates/blocks/DD-Team/Component.tsx +247 -247
  153. package/templates/blocks/DD-Team/config.ts +173 -173
  154. package/templates/blocks/DD-TeamCards/Component.tsx +251 -0
  155. package/templates/blocks/DD-TeamCards/README.md +4 -0
  156. package/templates/blocks/DD-TeamCards/config.ts +302 -0
  157. package/templates/blocks/DD-TeamCards/manifest.json +74 -0
  158. package/templates/blocks/DD-TechStack/Component.tsx +174 -174
  159. package/templates/blocks/DD-TechStack/config.ts +153 -153
  160. package/templates/blocks/DD-TestMarq/Component.tsx +132 -0
  161. package/templates/blocks/DD-TestMarq/README.md +4 -0
  162. package/templates/blocks/DD-TestMarq/RowLabel.tsx +9 -0
  163. package/templates/blocks/DD-TestMarq/config.ts +88 -0
  164. package/templates/blocks/DD-TestMarq/manifest.json +72 -0
  165. package/templates/blocks/DD-Testim/Component.tsx +247 -0
  166. package/templates/blocks/DD-Testim/README.md +4 -0
  167. package/templates/blocks/DD-Testim/config.ts +101 -0
  168. package/templates/blocks/DD-Testim/manifest.json +75 -0
  169. package/templates/blocks/DD-Testim2/Component.tsx +256 -0
  170. package/templates/blocks/DD-Testim2/README.md +4 -0
  171. package/templates/blocks/DD-Testim2/config.ts +100 -0
  172. package/templates/blocks/DD-Testim2/manifest.json +74 -0
  173. package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
  174. package/templates/blocks/DD-Testimonials/config.ts +120 -120
  175. package/templates/blocks/DD-TextMarq/Component.tsx +157 -0
  176. package/templates/blocks/DD-TextMarq/README.md +4 -0
  177. package/templates/blocks/DD-TextMarq/config.ts +237 -0
  178. package/templates/blocks/DD-TextMarq/manifest.json +82 -0
  179. package/templates/blocks/DD-Thanks/Component.tsx +501 -0
  180. package/templates/blocks/DD-Thanks/README.md +4 -0
  181. package/templates/blocks/DD-Thanks/config.ts +224 -0
  182. package/templates/blocks/DD-Thanks/manifest.json +72 -0
  183. package/templates/blocks/DD-VertMarq/Component.tsx +154 -0
  184. package/templates/blocks/DD-VertMarq/README.md +4 -0
  185. package/templates/blocks/DD-VertMarq/config.ts +236 -0
  186. package/templates/blocks/DD-VertMarq/dist/index.css +6 -0
  187. package/templates/blocks/DD-VertMarq/dist/index.min.css +1 -0
  188. package/templates/blocks/DD-VertMarq/manifest.json +74 -0
  189. package/templates/blocks/DD-Work/Component.tsx +318 -315
  190. package/templates/blocks/DD-Work/config.ts +165 -165
  191. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  192. package/templates/blocks/DDHG-CTASection2/Component.tsx +185 -0
  193. package/templates/blocks/DDHG-CTASection2/config.ts +140 -0
  194. package/templates/blocks/DDHG-CTASection2/manifest.json +45 -0
  195. package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -0
  196. package/templates/blocks/DDHG-HighlightArc/config.ts +85 -0
  197. package/templates/blocks/DDHG-HighlightArc/manifest.json +46 -0
  198. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -0
  199. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -0
  200. package/templates/blocks/DDHG-LogoMarquee/manifest.json +39 -0
  201. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -0
  202. package/templates/blocks/DDHG-NavCodeGrid/config.ts +64 -0
  203. package/templates/blocks/DDHG-NavCodeGrid/manifest.json +45 -0
  204. package/templates/blocks/DDHG-ProcessSec/Component.tsx +574 -0
  205. package/templates/blocks/DDHG-ProcessSec/config.ts +170 -0
  206. package/templates/blocks/DDHG-ProcessSec/manifest.json +56 -0
  207. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +164 -0
  208. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +390 -0
  209. package/templates/blocks/DDHG-ServiceDetails/config.ts +77 -0
  210. package/templates/blocks/DDHG-ServiceDetails/manifest.json +50 -0
  211. package/templates/blocks/DDHG-ServiceHero/Component.tsx +234 -0
  212. package/templates/blocks/DDHG-ServiceHero/config.ts +49 -0
  213. package/templates/blocks/DDHG-ServiceHero/manifest.json +26 -0
  214. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +277 -0
  215. package/templates/blocks/DDHG-ServiceOverview/config.ts +64 -0
  216. package/templates/blocks/DDHG-ServiceOverview/manifest.json +44 -0
  217. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +389 -0
  218. package/templates/blocks/DDHG-StatsFAQ/config.ts +204 -0
  219. package/templates/blocks/DDHG-StatsFAQ/manifest.json +45 -0
  220. package/templates/blocks/DDHG-TeamCards/Component.tsx +169 -0
  221. package/templates/blocks/DDHG-TeamCards/config.ts +48 -0
  222. package/templates/blocks/DDHG-TeamCards/manifest.json +29 -0
  223. package/templates/shared/src/components/BgLightning/index.tsx +242 -0
  224. package/templates/shared/src/components/ui/ShareButton/index.tsx +80 -0
  225. package/templates/shared/src/components/ui/anims/FadeOnScroll.tsx +48 -0
  226. package/templates/shared/src/components/ui/anims/FadeUpOnInView.tsx +63 -0
  227. package/templates/shared/src/components/ui/anims/GlareHover.tsx +111 -0
  228. package/templates/shared/src/components/ui/anims/LineAnims.tsx +81 -0
  229. package/templates/shared/src/components/ui/anims/TitleAnims01.tsx +54 -0
  230. package/templates/shared/src/components/ui/anims/animation-container.tsx +29 -0
  231. package/templates/shared/src/components/ui/anims/blur-in.tsx +26 -0
  232. package/templates/shared/src/components/ui/anims/counter.tsx +53 -0
  233. package/templates/shared/src/components/ui/anims/fade.tsx +83 -0
  234. package/templates/shared/src/components/ui/anims/gradual-spacing.tsx +39 -0
  235. package/templates/shared/src/components/ui/anims/pull-up.tsx +93 -0
  236. package/templates/shared/src/components/ui/anims/reveal-text.tsx +76 -0
  237. package/templates/shared/src/components/ui/anims/rotate-words.tsx +43 -0
  238. package/templates/shared/src/components/ui/anims/shiny-text.tsx +58 -0
  239. package/templates/shared/src/components/ui/anims/shuffle-text.tsx +85 -0
  240. package/templates/shared/src/components/ui/anims/split-text.tsx +211 -0
  241. package/templates/shared/src/components/ui/anims/staggered-fade.tsx +133 -0
  242. package/templates/shared/src/components/ui/anims/text-scroll.tsx +111 -0
  243. package/templates/shared/src/components/ui/anims/typing-effect.tsx +112 -0
  244. package/templates/shared/src/components/ui/button-dusted-fluid/index.tsx +1521 -0
  245. package/templates/shared/src/components/ui/separator.tsx +85 -0
  246. package/templates/shared/src/hooks/useBodyScrollLock.ts +97 -0
  247. package/templates/shared/src/hooks/useClickOutside.ts +28 -0
  248. package/templates/shared/src/utilities/renderCustomCSS.ts +5 -0
  249. package/templates/shared/src/utilities/smoothScroll.ts +65 -0
@@ -1,535 +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 { 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
- }
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
+ }