blocks-dusted 0.1.9 → 0.1.11

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 (248) hide show
  1. package/README.md +35 -0
  2. package/package.json +1 -1
  3. package/src/utils/paths.js +13 -13
  4. package/templates/blocks/DD-Accordion/Component.tsx +229 -0
  5. package/templates/blocks/DD-Accordion/README.md +3 -0
  6. package/templates/blocks/DD-Accordion/config.ts +73 -0
  7. package/templates/blocks/DD-Accordion/manifest.json +75 -0
  8. package/templates/blocks/DD-AnimText/Component.tsx +788 -0
  9. package/templates/blocks/DD-AnimText/README.md +3 -0
  10. package/templates/blocks/DD-AnimText/config.ts +295 -0
  11. package/templates/blocks/DD-AnimText/manifest.json +85 -0
  12. package/templates/blocks/DD-AsideNav/Component.tsx +321 -0
  13. package/templates/blocks/DD-AsideNav/config.ts +204 -0
  14. package/templates/blocks/DD-AsideNav/manifest.json +80 -0
  15. package/templates/blocks/DD-Banner/Component.tsx +51 -0
  16. package/templates/blocks/DD-Banner/config.ts +57 -0
  17. package/templates/blocks/DD-Banner/manifest.json +72 -0
  18. package/templates/blocks/DD-BgLightning/Component.tsx +44 -0
  19. package/templates/blocks/DD-BgLightning/README.md +3 -0
  20. package/templates/blocks/DD-BgLightning/config.ts +97 -0
  21. package/templates/blocks/DD-BgLightning/manifest.json +79 -0
  22. package/templates/blocks/DD-BookCall/Component.tsx +10 -11
  23. package/templates/blocks/DD-BookCall/config.ts +238 -238
  24. package/templates/blocks/DD-ButtonPop/Component.tsx +117 -0
  25. package/templates/blocks/DD-ButtonPop/config.ts +105 -0
  26. package/templates/blocks/DD-ButtonPop/manifest.json +88 -0
  27. package/templates/blocks/DD-ButtonPop/svg-icons/Dust2.tsx +167 -0
  28. package/templates/blocks/DD-Card/Component.tsx +251 -0
  29. package/templates/blocks/DD-Card/README.md +4 -0
  30. package/templates/blocks/DD-Card/config.ts +154 -0
  31. package/templates/blocks/DD-Card/manifest.json +82 -0
  32. package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -6
  33. package/templates/blocks/DD-CardTemplate01/config.ts +106 -106
  34. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -249
  35. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -403
  36. package/templates/blocks/DD-Chip/Component.tsx +250 -0
  37. package/templates/blocks/DD-Chip/EXAMPLES.md +313 -0
  38. package/templates/blocks/DD-Chip/README.md +202 -0
  39. package/templates/blocks/DD-Chip/config.ts +369 -0
  40. package/templates/blocks/DD-Chip/manifest.json +74 -0
  41. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -256
  42. package/templates/blocks/DD-Contact/Component.tsx +434 -434
  43. package/templates/blocks/DD-Contact/config.ts +22 -22
  44. package/templates/blocks/DD-Counter/Component.tsx +190 -0
  45. package/templates/blocks/DD-Counter/README.md +4 -0
  46. package/templates/blocks/DD-Counter/config.ts +291 -0
  47. package/templates/blocks/DD-Counter/manifest.json +74 -0
  48. package/templates/blocks/DD-CustomCSS/Component.tsx +8 -0
  49. package/templates/blocks/DD-CustomCSS/README.md +4 -0
  50. package/templates/blocks/DD-CustomCSS/addToLayout.md +5 -0
  51. package/templates/blocks/DD-CustomCSS/config.ts +22 -0
  52. package/templates/blocks/DD-CustomCSS/manifest.json +72 -0
  53. package/templates/blocks/DD-CustomCSS/renderCustomCSS.md +7 -0
  54. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  55. package/templates/blocks/DD-FeatCat/config.ts +201 -201
  56. package/templates/blocks/DD-FeatStrip/Component.tsx +585 -585
  57. package/templates/blocks/DD-FeatStrip/config.ts +134 -134
  58. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +628 -0
  59. package/templates/blocks/DD-FluidBtn/Component.tsx +43 -0
  60. package/templates/blocks/DD-FluidBtn/README.md +6 -0
  61. package/templates/blocks/DD-FluidBtn/config.ts +102 -0
  62. package/templates/blocks/DD-FluidBtn/manifest.json +86 -0
  63. package/templates/blocks/DD-GooeyInfo/Component.tsx +149 -0
  64. package/templates/blocks/DD-GooeyInfo/config.ts +135 -0
  65. package/templates/blocks/DD-GooeyInfo/manifest.json +72 -0
  66. package/templates/blocks/DD-Hero/Component.tsx +40 -17
  67. package/templates/blocks/DD-Hero/config.ts +267 -267
  68. package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -2
  69. package/templates/blocks/DD-HorizontalScroll/config.ts +231 -231
  70. package/templates/blocks/DD-Icon/Component.tsx +287 -0
  71. package/templates/blocks/DD-Icon/README.md +4 -0
  72. package/templates/blocks/DD-Icon/config.ts +258 -0
  73. package/templates/blocks/DD-Icon/hooks/revalidateBlockIds.ts +66 -0
  74. package/templates/blocks/DD-Icon/manifest.json +82 -0
  75. package/templates/blocks/DD-IconButton/Component.tsx +455 -0
  76. package/templates/blocks/DD-IconButton/README.md +326 -0
  77. package/templates/blocks/DD-IconButton/config.ts +239 -0
  78. package/templates/blocks/DD-IconButton/manifest.json +85 -0
  79. package/templates/blocks/DD-IdTag/Component.tsx +21 -0
  80. package/templates/blocks/DD-IdTag/README.md +4 -0
  81. package/templates/blocks/DD-IdTag/config.ts +41 -0
  82. package/templates/blocks/DD-IdTag/manifest.json +72 -0
  83. package/templates/blocks/DD-Iframe/Component.tsx +45 -0
  84. package/templates/blocks/DD-Iframe/README.md +4 -0
  85. package/templates/blocks/DD-Iframe/config.ts +47 -0
  86. package/templates/blocks/DD-Iframe/manifest.json +72 -0
  87. package/templates/blocks/DD-IframeMedia/Component.tsx +129 -0
  88. package/templates/blocks/DD-IframeMedia/README.md +4 -0
  89. package/templates/blocks/DD-IframeMedia/config.ts +79 -0
  90. package/templates/blocks/DD-IframeMedia/manifest.json +72 -0
  91. package/templates/blocks/DD-LineAnim/Component.tsx +36 -0
  92. package/templates/blocks/DD-LineAnim/README.md +4 -0
  93. package/templates/blocks/DD-LineAnim/config.ts +90 -0
  94. package/templates/blocks/DD-LineAnim/manifest.json +80 -0
  95. package/templates/blocks/DD-LinkGrid/Component.tsx +119 -0
  96. package/templates/blocks/DD-LinkGrid/README.md +4 -0
  97. package/templates/blocks/DD-LinkGrid/config.ts +51 -0
  98. package/templates/blocks/DD-LinkGrid/manifest.json +74 -0
  99. package/templates/blocks/DD-LogoMarq/Component.tsx +132 -0
  100. package/templates/blocks/DD-LogoMarq/README.md +4 -0
  101. package/templates/blocks/DD-LogoMarq/config.ts +264 -0
  102. package/templates/blocks/DD-LogoMarq/manifest.json +82 -0
  103. package/templates/blocks/DD-Lottie/Component.tsx +459 -0
  104. package/templates/blocks/DD-Lottie/README.md +4 -0
  105. package/templates/blocks/DD-Lottie/config.ts +379 -0
  106. package/templates/blocks/DD-Lottie/index.tsx +28 -0
  107. package/templates/blocks/DD-Lottie/manifest.json +83 -0
  108. package/templates/blocks/DD-MagicBento/Component.tsx +777 -0
  109. package/templates/blocks/DD-MagicBento/README.md +4 -0
  110. package/templates/blocks/DD-MagicBento/config.ts +264 -0
  111. package/templates/blocks/DD-MagicBento/manifest.json +74 -0
  112. package/templates/blocks/DD-MasonaryMedia/Component.tsx +547 -545
  113. package/templates/blocks/DD-MasonaryMedia/config.ts +2 -2
  114. package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -2
  115. package/templates/blocks/DD-Masonry/Component.tsx +645 -0
  116. package/templates/blocks/DD-Masonry/README.md +4 -0
  117. package/templates/blocks/DD-Masonry/config.ts +475 -0
  118. package/templates/blocks/DD-Masonry/manifest.json +82 -0
  119. package/templates/blocks/DD-Pricing/Component.tsx +372 -372
  120. package/templates/blocks/DD-Process/Component.tsx +149 -149
  121. package/templates/blocks/DD-Process/config.ts +150 -150
  122. package/templates/blocks/DD-Progress/Component.tsx +141 -0
  123. package/templates/blocks/DD-Progress/README.md +4 -0
  124. package/templates/blocks/DD-Progress/config.ts +175 -0
  125. package/templates/blocks/DD-Progress/manifest.json +74 -0
  126. package/templates/blocks/DD-ProjHeader/Component.tsx +91 -0
  127. package/templates/blocks/DD-ProjHeader/README.md +4 -0
  128. package/templates/blocks/DD-ProjHeader/config.ts +96 -0
  129. package/templates/blocks/DD-ProjHeader/manifest.json +72 -0
  130. package/templates/blocks/DD-ProjSlider/Component.tsx +123 -0
  131. package/templates/blocks/DD-ProjSlider/README.md +4 -0
  132. package/templates/blocks/DD-ProjSlider/config.ts +64 -0
  133. package/templates/blocks/DD-ProjSlider/manifest.json +72 -0
  134. package/templates/blocks/DD-ScrollText/Component.tsx +114 -0
  135. package/templates/blocks/DD-ScrollText/README.md +4 -0
  136. package/templates/blocks/DD-ScrollText/config.ts +58 -0
  137. package/templates/blocks/DD-ScrollText/manifest.json +72 -0
  138. package/templates/blocks/DD-Section/Component.tsx +266 -266
  139. package/templates/blocks/DD-Separator/Component.tsx +66 -0
  140. package/templates/blocks/DD-Separator/README.md +4 -0
  141. package/templates/blocks/DD-Separator/config.ts +58 -0
  142. package/templates/blocks/DD-Separator/manifest.json +72 -0
  143. package/templates/blocks/DD-Services/Component.tsx +176 -176
  144. package/templates/blocks/DD-Services/config.ts +149 -149
  145. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -307
  146. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  147. package/templates/blocks/DD-StackCards/Component.tsx +473 -473
  148. package/templates/blocks/DD-StackCards/config.ts +399 -399
  149. package/templates/blocks/DD-Team/Component.tsx +247 -247
  150. package/templates/blocks/DD-Team/config.ts +173 -173
  151. package/templates/blocks/DD-TeamCards/Component.tsx +251 -0
  152. package/templates/blocks/DD-TeamCards/README.md +4 -0
  153. package/templates/blocks/DD-TeamCards/config.ts +302 -0
  154. package/templates/blocks/DD-TeamCards/manifest.json +74 -0
  155. package/templates/blocks/DD-TechStack/Component.tsx +168 -162
  156. package/templates/blocks/DD-TechStack/config.ts +153 -153
  157. package/templates/blocks/DD-TestMarq/Component.tsx +132 -0
  158. package/templates/blocks/DD-TestMarq/README.md +4 -0
  159. package/templates/blocks/DD-TestMarq/RowLabel.tsx +9 -0
  160. package/templates/blocks/DD-TestMarq/config.ts +88 -0
  161. package/templates/blocks/DD-TestMarq/manifest.json +72 -0
  162. package/templates/blocks/DD-Testim/Component.tsx +247 -0
  163. package/templates/blocks/DD-Testim/README.md +4 -0
  164. package/templates/blocks/DD-Testim/config.ts +101 -0
  165. package/templates/blocks/DD-Testim/manifest.json +75 -0
  166. package/templates/blocks/DD-Testim2/Component.tsx +256 -0
  167. package/templates/blocks/DD-Testim2/README.md +4 -0
  168. package/templates/blocks/DD-Testim2/config.ts +100 -0
  169. package/templates/blocks/DD-Testim2/manifest.json +74 -0
  170. package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
  171. package/templates/blocks/DD-Testimonials/config.ts +120 -120
  172. package/templates/blocks/DD-TextMarq/Component.tsx +157 -0
  173. package/templates/blocks/DD-TextMarq/README.md +4 -0
  174. package/templates/blocks/DD-TextMarq/config.ts +237 -0
  175. package/templates/blocks/DD-TextMarq/manifest.json +82 -0
  176. package/templates/blocks/DD-Thanks/Component.tsx +500 -0
  177. package/templates/blocks/DD-Thanks/README.md +4 -0
  178. package/templates/blocks/DD-Thanks/config.ts +224 -0
  179. package/templates/blocks/DD-Thanks/manifest.json +72 -0
  180. package/templates/blocks/DD-VertMarq/Component.tsx +154 -0
  181. package/templates/blocks/DD-VertMarq/README.md +4 -0
  182. package/templates/blocks/DD-VertMarq/config.ts +236 -0
  183. package/templates/blocks/DD-VertMarq/dist/index.css +6 -0
  184. package/templates/blocks/DD-VertMarq/dist/index.min.css +1 -0
  185. package/templates/blocks/DD-VertMarq/manifest.json +74 -0
  186. package/templates/blocks/DD-Work/Component.tsx +318 -315
  187. package/templates/blocks/DD-Work/config.ts +165 -165
  188. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  189. package/templates/blocks/DDHG-CTASection2/Component.tsx +185 -0
  190. package/templates/blocks/DDHG-CTASection2/config.ts +140 -0
  191. package/templates/blocks/DDHG-CTASection2/manifest.json +45 -0
  192. package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -0
  193. package/templates/blocks/DDHG-HighlightArc/config.ts +85 -0
  194. package/templates/blocks/DDHG-HighlightArc/manifest.json +46 -0
  195. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +93 -0
  196. package/templates/blocks/DDHG-LogoMarquee/config.ts +66 -0
  197. package/templates/blocks/DDHG-LogoMarquee/manifest.json +39 -0
  198. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +213 -0
  199. package/templates/blocks/DDHG-NavCodeGrid/config.ts +64 -0
  200. package/templates/blocks/DDHG-NavCodeGrid/manifest.json +45 -0
  201. package/templates/blocks/DDHG-ProcessSec/Component.tsx +575 -0
  202. package/templates/blocks/DDHG-ProcessSec/config.ts +170 -0
  203. package/templates/blocks/DDHG-ProcessSec/manifest.json +56 -0
  204. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +164 -0
  205. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +394 -0
  206. package/templates/blocks/DDHG-ServiceDetails/config.ts +77 -0
  207. package/templates/blocks/DDHG-ServiceDetails/manifest.json +50 -0
  208. package/templates/blocks/DDHG-ServiceHero/Component.tsx +234 -0
  209. package/templates/blocks/DDHG-ServiceHero/config.ts +49 -0
  210. package/templates/blocks/DDHG-ServiceHero/manifest.json +26 -0
  211. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +277 -0
  212. package/templates/blocks/DDHG-ServiceOverview/config.ts +64 -0
  213. package/templates/blocks/DDHG-ServiceOverview/manifest.json +44 -0
  214. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +389 -0
  215. package/templates/blocks/DDHG-StatsFAQ/config.ts +204 -0
  216. package/templates/blocks/DDHG-StatsFAQ/manifest.json +45 -0
  217. package/templates/blocks/DDHG-TeamCards/Component.tsx +131 -0
  218. package/templates/blocks/DDHG-TeamCards/config.ts +48 -0
  219. package/templates/blocks/DDHG-TeamCards/manifest.json +29 -0
  220. package/templates/components/HeaderDD02/MobileBottomNav/index.tsx +352 -257
  221. package/templates/components/HeaderDD02/index.tsx +313 -229
  222. package/templates/shared/src/components/BgLightning/index.tsx +242 -0
  223. package/templates/shared/src/components/ui/ShareButton/index.tsx +80 -0
  224. package/templates/shared/src/components/ui/anims/FadeOnScroll.tsx +48 -0
  225. package/templates/shared/src/components/ui/anims/FadeUpOnInView.tsx +63 -0
  226. package/templates/shared/src/components/ui/anims/GlareHover.tsx +111 -0
  227. package/templates/shared/src/components/ui/anims/LineAnims.tsx +81 -0
  228. package/templates/shared/src/components/ui/anims/TitleAnims01.tsx +54 -0
  229. package/templates/shared/src/components/ui/anims/animation-container.tsx +29 -0
  230. package/templates/shared/src/components/ui/anims/blur-in.tsx +26 -0
  231. package/templates/shared/src/components/ui/anims/counter.tsx +53 -0
  232. package/templates/shared/src/components/ui/anims/fade.tsx +83 -0
  233. package/templates/shared/src/components/ui/anims/gradual-spacing.tsx +39 -0
  234. package/templates/shared/src/components/ui/anims/pull-up.tsx +93 -0
  235. package/templates/shared/src/components/ui/anims/reveal-text.tsx +76 -0
  236. package/templates/shared/src/components/ui/anims/rotate-words.tsx +43 -0
  237. package/templates/shared/src/components/ui/anims/shiny-text.tsx +58 -0
  238. package/templates/shared/src/components/ui/anims/shuffle-text.tsx +85 -0
  239. package/templates/shared/src/components/ui/anims/split-text.tsx +211 -0
  240. package/templates/shared/src/components/ui/anims/staggered-fade.tsx +133 -0
  241. package/templates/shared/src/components/ui/anims/text-scroll.tsx +111 -0
  242. package/templates/shared/src/components/ui/anims/typing-effect.tsx +112 -0
  243. package/templates/shared/src/components/ui/button-dusted-fluid/index.tsx +1521 -0
  244. package/templates/shared/src/components/ui/separator.tsx +85 -0
  245. package/templates/shared/src/hooks/useBodyScrollLock.ts +97 -0
  246. package/templates/shared/src/hooks/useClickOutside.ts +28 -0
  247. package/templates/shared/src/utilities/renderCustomCSS.ts +5 -0
  248. package/templates/shared/src/utilities/smoothScroll.ts +65 -0
@@ -1,434 +1,434 @@
1
- 'use client'
2
- import React, { type FormEvent, useMemo, useState } from 'react'
3
- import * as LucideIcons from 'lucide-react'
4
- import { Check, Loader2, Send } from 'lucide-react'
5
- import { Input } from '@/components/ui/input'
6
- import { getClientSideURL } from '@/utilities/getURL'
7
-
8
- type Status = 'idle' | 'submitting' | 'success' | 'error'
9
- export type DDContactProps = {
10
- eyebrow?: string | null
11
- headlineMain?: string | null
12
- headlineAccent?: string | null
13
- description?: string | null
14
- infoItems?: Array<{
15
- iconName?: string | null
16
- title: string
17
- description: string
18
- }> | null
19
- availabilityActive?: boolean | null
20
- availabilityLabel?: string | null
21
- availabilitySubtext?: string | null
22
- form?: string | number | { id?: string | number } | null
23
- serviceOptions?: Array<{ label: string }> | null
24
- budgetOptions?: Array<{ label: string }> | null
25
- timelineOptions?: Array<{ label: string }> | null
26
- sectionId?: string | null
27
- customCSS?: string | null
28
- }
29
- function DynamicIcon({
30
- name,
31
- size = 16,
32
- ...props
33
- }: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
34
- if (!name) return null
35
- const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
36
- if (!Icon) return null
37
- return <Icon size={size} {...props} />
38
- }
39
-
40
- export const DDContact: React.FC<DDContactProps> = ({
41
- eyebrow,
42
- headlineMain,
43
- headlineAccent,
44
- description,
45
- infoItems,
46
- availabilityActive,
47
- availabilityLabel,
48
- availabilitySubtext,
49
- form,
50
- serviceOptions,
51
- budgetOptions,
52
- timelineOptions,
53
- sectionId,
54
- customCSS,
55
- }) => {
56
- const [status, setStatus] = useState<Status>('idle')
57
- const [charCount, setCharCount] = useState(0)
58
-
59
- const formID = typeof form === 'object' && form !== null ? form.id : form
60
-
61
- const formIdentifier = useMemo(() => {
62
- const safeSection = (sectionId || 'contact').toLowerCase().replace(/[^a-z0-9-_]/g, '-')
63
-
64
- return `studio-contact-${String(formID || 'unknown')}-${safeSection}`
65
- }, [formID, sectionId])
66
-
67
- const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
68
- e.preventDefault()
69
- const formElement = e.currentTarget
70
- const data = new FormData(formElement)
71
-
72
- const message = data.get('message') as string
73
- if (message && message.length > 500) return
74
-
75
- setStatus('submitting')
76
-
77
- const submissionData = [
78
- { field: 'name', value: data.get('name') },
79
- { field: 'email', value: data.get('email') },
80
- { field: 'company', value: data.get('company') },
81
- { field: 'service', value: data.get('service') },
82
- { field: 'budget', value: data.get('budget') },
83
- { field: 'timeline', value: data.get('timeline') },
84
- { field: 'message', value: data.get('message') },
85
- ]
86
-
87
- if (!formID) {
88
- setStatus('success')
89
- formElement.reset()
90
- setCharCount(0)
91
- return
92
- }
93
-
94
- try {
95
- const res = await fetch(`${getClientSideURL()}/api/form-submissions`, {
96
- method: 'POST',
97
- headers: {
98
- 'Content-Type': 'application/json',
99
- },
100
- body: JSON.stringify({
101
- form: formID,
102
- submissionData,
103
- }),
104
- })
105
-
106
- if (res.ok) {
107
- setStatus('success')
108
- formElement.reset()
109
- setCharCount(0)
110
- } else {
111
- setStatus('error')
112
- }
113
- } catch {
114
- setStatus('error')
115
- }
116
- }
117
-
118
- return (
119
- <>
120
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
121
-
122
- <div id={sectionId ?? 'contact'} className="py-24">
123
- <div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
124
- #BlockName-DDContact
125
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
126
- <div>
127
- {eyebrow && (
128
- <p className="text-[11.5px] font-semibold text-stone-400 tracking-widest uppercase mb-3 top-caption">
129
- {eyebrow}
130
- </p>
131
- )}
132
-
133
- <h2 className="text-4xl md:text-5xl font-semibold text-stone-900 dark:text-stone-50 leading-tight mb-6">
134
- {headlineMain}
135
- <br />
136
- <span className="text-stone-400">{headlineAccent}</span>
137
- </h2>
138
-
139
- {description && (
140
- <p className="text-stone-700 dark:text-stone-300 text-base leading-relaxed mb-10">
141
- {description}
142
- </p>
143
- )}
144
-
145
- {infoItems && infoItems.length > 0 && (
146
- <div className="space-y-4 mb-10">
147
- {infoItems.map((item, i) => (
148
- <div key={i} className="flex gap-4">
149
- <div className="w-9 h-9 flex items-center justify-center rounded-lg lime-glass border border-indigo-400 bg-stone-100/50 dark:bg-stone-900/80 shrink-0 mt-0.5">
150
- <DynamicIcon
151
- className="text-indigo-500 dark:text-lime-500"
152
- name={item.iconName}
153
- size={15}
154
- />
155
- </div>
156
- <div>
157
- <div className="text-sm font-semibold text-stone-800 dark:text-stone-50 mb-0.5">
158
- {item.title}
159
- </div>
160
- <div className="text-xs text-stone-600 dark:text-stone-300">
161
- {item.description}
162
- </div>
163
- </div>
164
- </div>
165
- ))}
166
- </div>
167
- )}
168
-
169
- {availabilityActive && (
170
- <div
171
- className="flex items-center gap-3 p-4 bg-indigo-900 border border-indigo-500 shadow-xl backdrop-blur-sm rounded-2xl pressable relative z-10 h-full w-full text-[32px] font-medium tracking-[.001em] transition-all duration-300"
172
- style={{ cornerShape: 'squircle' } as React.CSSProperties}
173
- >
174
- <div className="w-3.5 h-3.5 rounded-full bg-lime-700 border-2 border-lime-500 animate-pulse shrink-0 shadow-2xl" />
175
- <div>
176
- {availabilityLabel && (
177
- <div className="text-md font-semibold text-lime-500 dark:text-stone-50">
178
- {availabilityLabel}
179
- </div>
180
- )}
181
-
182
- {availabilitySubtext && (
183
- <div className="text-xs font-normal! text-stone-300 pt-1">
184
- {availabilitySubtext}
185
- </div>
186
- )}
187
- </div>
188
- </div>
189
- )}
190
- </div>
191
-
192
- <div className="rounded-2xl bg-stone-100 dark:bg-stone-950 border border-stone-100 dark:border-stone-800 dark:hover:border-indigo-800 px-3 md:px-8 py-8 shadow-sm hover:shadow-2xl transition-all duration-300">
193
- {status === 'success' ? (
194
- <div className="flex flex-col items-center justify-center text-center py-12">
195
- <div className="w-16 h-16 flex items-center justify-center rounded-full bg-lime-400/10 border border-emerald-400/30 mb-5">
196
- <Check size={28} className="text-lime-400" />
197
- </div>
198
-
199
- <h3 className="text-xl font-semibold text-stone-900 dark:text-stone-50 mb-2">
200
- Message sent!
201
- </h3>
202
-
203
- <p className="text-sm text-stone-400 max-w-xs">
204
- Thanks for reaching out. We'll review your project details and get back to you
205
- within one business day.
206
- </p>
207
-
208
- <button
209
- onClick={() => setStatus('idle')}
210
- className="mt-6 px-5 py-2.5 rounded-lgbg-stone-700 text-stone-200 text-sm font-medium hover:bg-stone-600 transition-colors cursor-pointer"
211
- >
212
- Send another message
213
- </button>
214
- </div>
215
- ) : (
216
- <form
217
- id={formIdentifier}
218
- name={formIdentifier}
219
- data-form-id={String(formID || '')}
220
- onSubmit={handleSubmit}
221
- className="space-y-5"
222
- >
223
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
224
- <div>
225
- <label
226
- htmlFor={`${formIdentifier}-name`}
227
- className="block text-xs font-medium text-stone-400 mb-1.5"
228
- >
229
- Your Name *
230
- </label>
231
-
232
- <Input
233
- id={`${formIdentifier}-name`}
234
- type="text"
235
- name="name"
236
- autoComplete="name"
237
- required
238
- placeholder="Marcus Holt"
239
- className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
240
- />
241
- </div>
242
-
243
- <div>
244
- <label
245
- htmlFor={`${formIdentifier}-email`}
246
- className="block text-xs font-medium text-stone-400 mb-1.5"
247
- >
248
- Email Address *
249
- </label>
250
-
251
- <Input
252
- id={`${formIdentifier}-email`}
253
- type="email"
254
- name="email"
255
- autoComplete="email"
256
- required
257
- placeholder="you@company.com"
258
- className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
259
- />
260
- </div>
261
- </div>
262
-
263
- <div>
264
- <label
265
- htmlFor={`${formIdentifier}-company`}
266
- className="block text-xs font-medium text-stone-400 mb-1.5"
267
- >
268
- Company / Project Name
269
- </label>
270
-
271
- <Input
272
- id={`${formIdentifier}-company`}
273
- type="text"
274
- name="company"
275
- autoComplete="organization"
276
- placeholder="Acme Inc."
277
- className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
278
- />
279
- </div>
280
-
281
- {serviceOptions && serviceOptions.length > 0 && (
282
- <div>
283
- <label
284
- htmlFor={`${formIdentifier}-service`}
285
- className="block text-xs font-medium text-stone-400 mb-1.5"
286
- >
287
- Service Needed *
288
- </label>
289
-
290
- <select
291
- id={`${formIdentifier}-service`}
292
- name="service"
293
- autoComplete="off"
294
- required
295
- defaultValue=""
296
- className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
297
- >
298
- <option value="" disabled>
299
- Select a service...
300
- </option>
301
-
302
- {serviceOptions.map((s, i) => (
303
- <option key={i} value={s.label}>
304
- {s.label}
305
- </option>
306
- ))}
307
- </select>
308
- </div>
309
- )}
310
-
311
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
312
- {budgetOptions && budgetOptions.length > 0 && (
313
- <div>
314
- <label
315
- htmlFor={`${formIdentifier}-budget`}
316
- className="block text-xs font-medium text-stone-400 mb-1.5"
317
- >
318
- Budget Range *
319
- </label>
320
-
321
- <select
322
- id={`${formIdentifier}-budget`}
323
- name="budget"
324
- autoComplete="off"
325
- required
326
- defaultValue=""
327
- className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
328
- >
329
- <option value="" disabled>
330
- Select budget...
331
- </option>
332
-
333
- {budgetOptions.map((b, i) => (
334
- <option key={i} value={b.label}>
335
- {b.label}
336
- </option>
337
- ))}
338
- </select>
339
- </div>
340
- )}
341
-
342
- {timelineOptions && timelineOptions.length > 0 && (
343
- <div>
344
- <label
345
- htmlFor={`${formIdentifier}-timeline`}
346
- className="block text-xs font-medium text-stone-400 mb-1.5"
347
- >
348
- Timeline
349
- </label>
350
-
351
- <select
352
- id={`${formIdentifier}-timeline`}
353
- name="timeline"
354
- autoComplete="off"
355
- defaultValue=""
356
- className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
357
- >
358
- <option value="" disabled>
359
- Select timeline...
360
- </option>
361
-
362
- {timelineOptions.map((t, i) => (
363
- <option key={i} value={t.label}>
364
- {t.label}
365
- </option>
366
- ))}
367
- </select>
368
- </div>
369
- )}
370
- </div>
371
-
372
- <div>
373
- <label
374
- htmlFor={`${formIdentifier}-message`}
375
- className="block text-xs font-medium text-stone-400 mb-1.5"
376
- >
377
- Project Description *
378
- <span
379
- className={`ml-2 font-normal ${charCount > 500 ? 'text-red-400' : 'text-stone-400'}`}
380
- >
381
- {charCount}/500
382
- </span>
383
- </label>
384
-
385
- <textarea
386
- id={`${formIdentifier}-message`}
387
- name="message"
388
- autoComplete="off"
389
- required
390
- rows={4}
391
- maxLength={500}
392
- placeholder="Tell us about your project — what you're building, what problem it solves, and any technical constraints..."
393
- onChange={(e) => setCharCount(e.target.value.length)}
394
- className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
395
- />
396
- </div>
397
-
398
- {status === 'error' && (
399
- <p className="text-xs text-red-400">Something went wrong. Please try again.</p>
400
- )}
401
-
402
- <button
403
- type="submit"
404
- disabled={status === 'submitting' || charCount > 500}
405
- className="glass-button-white group relative isolate flex w-full cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-4xl! border border-white/30 dark:border-white bg- px-6 py-3 text-sm font-semibold text-stone-100 dark:text-stone-900 bg-stone-950/90 dark:bg-stone-100/80 outline-offset-0 dark:hover:bg-stone-100 hover:shadow-2xl hover:-translate-y-0.5 hover:outline-4 hover:outline-indigo-500 hover:-outline-offset-1 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-300"
406
- style={{ cornerShape: 'squircle' } as React.CSSProperties}
407
- >
408
- <span className="relative z-2 flex items-center gap-2">
409
- {status === 'submitting' ? (
410
- <>
411
- <Loader2 size={15} className="animate-spin" />
412
- Sending...
413
- </>
414
- ) : (
415
- <>
416
- <Send size={15} />
417
- Send Project Brief
418
- </>
419
- )}
420
- </span>
421
- </button>
422
-
423
- <p className="text-xs text-stone-700 dark:text-stone-400 text-center">
424
- We'll respond within 1 business day. No spam, ever.
425
- </p>
426
- </form>
427
- )}
428
- </div>
429
- </div>
430
- </div>
431
- </div>
432
- </>
433
- )
434
- }
1
+ 'use client'
2
+ import React, { type FormEvent, useMemo, useState } from 'react'
3
+ import * as LucideIcons from 'lucide-react'
4
+ import { Check, Loader2, Send } from 'lucide-react'
5
+ import { Input } from '@/components/ui/input'
6
+ import { getClientSideURL } from '@/utilities/getURL'
7
+
8
+ type Status = 'idle' | 'submitting' | 'success' | 'error'
9
+ export type DDContactProps = {
10
+ eyebrow?: string | null
11
+ headlineMain?: string | null
12
+ headlineAccent?: string | null
13
+ description?: string | null
14
+ infoItems?: Array<{
15
+ iconName?: string | null
16
+ title: string
17
+ description: string
18
+ }> | null
19
+ availabilityActive?: boolean | null
20
+ availabilityLabel?: string | null
21
+ availabilitySubtext?: string | null
22
+ form?: string | number | { id?: string | number } | null
23
+ serviceOptions?: Array<{ label: string }> | null
24
+ budgetOptions?: Array<{ label: string }> | null
25
+ timelineOptions?: Array<{ label: string }> | null
26
+ sectionId?: string | null
27
+ customCSS?: string | null
28
+ }
29
+ function DynamicIcon({
30
+ name,
31
+ size = 16,
32
+ ...props
33
+ }: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
34
+ if (!name) return null
35
+ const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
36
+ if (!Icon) return null
37
+ return <Icon size={size} {...props} />
38
+ }
39
+
40
+ export const DDContact: React.FC<DDContactProps> = ({
41
+ eyebrow,
42
+ headlineMain,
43
+ headlineAccent,
44
+ description,
45
+ infoItems,
46
+ availabilityActive,
47
+ availabilityLabel,
48
+ availabilitySubtext,
49
+ form,
50
+ serviceOptions,
51
+ budgetOptions,
52
+ timelineOptions,
53
+ sectionId,
54
+ customCSS,
55
+ }) => {
56
+ const [status, setStatus] = useState<Status>('idle')
57
+ const [charCount, setCharCount] = useState(0)
58
+
59
+ const formID = typeof form === 'object' && form !== null ? form.id : form
60
+
61
+ const formIdentifier = useMemo(() => {
62
+ const safeSection = (sectionId || 'contact').toLowerCase().replace(/[^a-z0-9-_]/g, '-')
63
+
64
+ return `studio-contact-${String(formID || 'unknown')}-${safeSection}`
65
+ }, [formID, sectionId])
66
+
67
+ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
68
+ e.preventDefault()
69
+ const formElement = e.currentTarget
70
+ const data = new FormData(formElement)
71
+
72
+ const message = data.get('message') as string
73
+ if (message && message.length > 500) return
74
+
75
+ setStatus('submitting')
76
+
77
+ const submissionData = [
78
+ { field: 'name', value: data.get('name') },
79
+ { field: 'email', value: data.get('email') },
80
+ { field: 'company', value: data.get('company') },
81
+ { field: 'service', value: data.get('service') },
82
+ { field: 'budget', value: data.get('budget') },
83
+ { field: 'timeline', value: data.get('timeline') },
84
+ { field: 'message', value: data.get('message') },
85
+ ]
86
+
87
+ if (!formID) {
88
+ setStatus('success')
89
+ formElement.reset()
90
+ setCharCount(0)
91
+ return
92
+ }
93
+
94
+ try {
95
+ const res = await fetch(`${getClientSideURL()}/api/form-submissions`, {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ },
100
+ body: JSON.stringify({
101
+ form: formID,
102
+ submissionData,
103
+ }),
104
+ })
105
+
106
+ if (res.ok) {
107
+ setStatus('success')
108
+ formElement.reset()
109
+ setCharCount(0)
110
+ } else {
111
+ setStatus('error')
112
+ }
113
+ } catch {
114
+ setStatus('error')
115
+ }
116
+ }
117
+
118
+ return (
119
+ <>
120
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
121
+
122
+ <div id={sectionId ?? 'contact'} className="py-24">
123
+ <div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
124
+
125
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
126
+ <div>
127
+ {eyebrow && (
128
+ <p className="text-[11.5px] font-semibold text-stone-400 tracking-widest uppercase mb-3 top-caption">
129
+ {eyebrow}
130
+ </p>
131
+ )}
132
+
133
+ <h2 className="text-4xl md:text-5xl font-semibold text-stone-900 dark:text-stone-50 leading-tight mb-6">
134
+ {headlineMain}
135
+ <br />
136
+ <span className="text-stone-400">{headlineAccent}</span>
137
+ </h2>
138
+
139
+ {description && (
140
+ <p className="text-stone-700 dark:text-stone-300 text-base leading-relaxed mb-10">
141
+ {description}
142
+ </p>
143
+ )}
144
+
145
+ {infoItems && infoItems.length > 0 && (
146
+ <div className="space-y-4 mb-10">
147
+ {infoItems.map((item, i) => (
148
+ <div key={i} className="flex gap-4">
149
+ <div className="w-9 h-9 flex items-center justify-center rounded-lg lime-glass border border-indigo-400 bg-stone-100/50 dark:bg-stone-900/80 shrink-0 mt-0.5">
150
+ <DynamicIcon
151
+ className="text-indigo-500 dark:text-lime-500"
152
+ name={item.iconName}
153
+ size={15}
154
+ />
155
+ </div>
156
+ <div>
157
+ <div className="text-sm font-semibold text-stone-800 dark:text-stone-50 mb-0.5">
158
+ {item.title}
159
+ </div>
160
+ <div className="text-xs text-stone-600 dark:text-stone-300">
161
+ {item.description}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ ))}
166
+ </div>
167
+ )}
168
+
169
+ {availabilityActive && (
170
+ <div
171
+ className="flex items-center gap-3 p-4 bg-indigo-900 border border-indigo-500 shadow-xl backdrop-blur-sm rounded-2xl pressable relative z-10 h-full w-full text-[32px] font-medium tracking-[.001em] transition-all duration-300"
172
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
173
+ >
174
+ <div className="w-3.5 h-3.5 rounded-full bg-lime-700 border-2 border-lime-500 animate-pulse shrink-0 shadow-2xl" />
175
+ <div>
176
+ {availabilityLabel && (
177
+ <div className="text-md font-semibold text-lime-500 dark:text-stone-50">
178
+ {availabilityLabel}
179
+ </div>
180
+ )}
181
+
182
+ {availabilitySubtext && (
183
+ <div className="text-xs font-normal! text-stone-300 pt-1">
184
+ {availabilitySubtext}
185
+ </div>
186
+ )}
187
+ </div>
188
+ </div>
189
+ )}
190
+ </div>
191
+
192
+ <div className="rounded-2xl bg-stone-100 dark:bg-stone-950 border border-stone-100 dark:border-stone-800 dark:hover:border-indigo-800 px-3 md:px-8 py-8 shadow-sm hover:shadow-2xl transition-all duration-300">
193
+ {status === 'success' ? (
194
+ <div className="flex flex-col items-center justify-center text-center py-12">
195
+ <div className="w-16 h-16 flex items-center justify-center rounded-full bg-lime-400/10 border border-emerald-400/30 mb-5">
196
+ <Check size={28} className="text-lime-400" />
197
+ </div>
198
+
199
+ <h3 className="text-xl font-semibold text-stone-900 dark:text-stone-50 mb-2">
200
+ Message sent!
201
+ </h3>
202
+
203
+ <p className="text-sm text-stone-400 max-w-xs">
204
+ Thanks for reaching out. We'll review your project details and get back to you
205
+ within one business day.
206
+ </p>
207
+
208
+ <button
209
+ onClick={() => setStatus('idle')}
210
+ className="mt-6 px-5 py-2.5 rounded-lgbg-stone-700 text-stone-200 text-sm font-medium hover:bg-stone-600 transition-colors cursor-pointer"
211
+ >
212
+ Send another message
213
+ </button>
214
+ </div>
215
+ ) : (
216
+ <form
217
+ id={formIdentifier}
218
+ name={formIdentifier}
219
+ data-form-id={String(formID || '')}
220
+ onSubmit={handleSubmit}
221
+ className="space-y-5"
222
+ >
223
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
224
+ <div>
225
+ <label
226
+ htmlFor={`${formIdentifier}-name`}
227
+ className="block text-xs font-medium text-stone-400 mb-1.5"
228
+ >
229
+ Your Name *
230
+ </label>
231
+
232
+ <Input
233
+ id={`${formIdentifier}-name`}
234
+ type="text"
235
+ name="name"
236
+ autoComplete="name"
237
+ required
238
+ placeholder="Marcus Holt"
239
+ className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
240
+ />
241
+ </div>
242
+
243
+ <div>
244
+ <label
245
+ htmlFor={`${formIdentifier}-email`}
246
+ className="block text-xs font-medium text-stone-400 mb-1.5"
247
+ >
248
+ Email Address *
249
+ </label>
250
+
251
+ <Input
252
+ id={`${formIdentifier}-email`}
253
+ type="email"
254
+ name="email"
255
+ autoComplete="email"
256
+ required
257
+ placeholder="you@company.com"
258
+ className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
259
+ />
260
+ </div>
261
+ </div>
262
+
263
+ <div>
264
+ <label
265
+ htmlFor={`${formIdentifier}-company`}
266
+ className="block text-xs font-medium text-stone-400 mb-1.5"
267
+ >
268
+ Company / Project Name
269
+ </label>
270
+
271
+ <Input
272
+ id={`${formIdentifier}-company`}
273
+ type="text"
274
+ name="company"
275
+ autoComplete="organization"
276
+ placeholder="Acme Inc."
277
+ className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
278
+ />
279
+ </div>
280
+
281
+ {serviceOptions && serviceOptions.length > 0 && (
282
+ <div>
283
+ <label
284
+ htmlFor={`${formIdentifier}-service`}
285
+ className="block text-xs font-medium text-stone-400 mb-1.5"
286
+ >
287
+ Service Needed *
288
+ </label>
289
+
290
+ <select
291
+ id={`${formIdentifier}-service`}
292
+ name="service"
293
+ autoComplete="off"
294
+ required
295
+ defaultValue=""
296
+ className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
297
+ >
298
+ <option value="" disabled>
299
+ Select a service...
300
+ </option>
301
+
302
+ {serviceOptions.map((s, i) => (
303
+ <option key={i} value={s.label}>
304
+ {s.label}
305
+ </option>
306
+ ))}
307
+ </select>
308
+ </div>
309
+ )}
310
+
311
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
312
+ {budgetOptions && budgetOptions.length > 0 && (
313
+ <div>
314
+ <label
315
+ htmlFor={`${formIdentifier}-budget`}
316
+ className="block text-xs font-medium text-stone-400 mb-1.5"
317
+ >
318
+ Budget Range *
319
+ </label>
320
+
321
+ <select
322
+ id={`${formIdentifier}-budget`}
323
+ name="budget"
324
+ autoComplete="off"
325
+ required
326
+ defaultValue=""
327
+ className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
328
+ >
329
+ <option value="" disabled>
330
+ Select budget...
331
+ </option>
332
+
333
+ {budgetOptions.map((b, i) => (
334
+ <option key={i} value={b.label}>
335
+ {b.label}
336
+ </option>
337
+ ))}
338
+ </select>
339
+ </div>
340
+ )}
341
+
342
+ {timelineOptions && timelineOptions.length > 0 && (
343
+ <div>
344
+ <label
345
+ htmlFor={`${formIdentifier}-timeline`}
346
+ className="block text-xs font-medium text-stone-400 mb-1.5"
347
+ >
348
+ Timeline
349
+ </label>
350
+
351
+ <select
352
+ id={`${formIdentifier}-timeline`}
353
+ name="timeline"
354
+ autoComplete="off"
355
+ defaultValue=""
356
+ className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
357
+ >
358
+ <option value="" disabled>
359
+ Select timeline...
360
+ </option>
361
+
362
+ {timelineOptions.map((t, i) => (
363
+ <option key={i} value={t.label}>
364
+ {t.label}
365
+ </option>
366
+ ))}
367
+ </select>
368
+ </div>
369
+ )}
370
+ </div>
371
+
372
+ <div>
373
+ <label
374
+ htmlFor={`${formIdentifier}-message`}
375
+ className="block text-xs font-medium text-stone-400 mb-1.5"
376
+ >
377
+ Project Description *
378
+ <span
379
+ className={`ml-2 font-normal ${charCount > 500 ? 'text-red-400' : 'text-stone-400'}`}
380
+ >
381
+ {charCount}/500
382
+ </span>
383
+ </label>
384
+
385
+ <textarea
386
+ id={`${formIdentifier}-message`}
387
+ name="message"
388
+ autoComplete="off"
389
+ required
390
+ rows={4}
391
+ maxLength={500}
392
+ placeholder="Tell us about your project — what you're building, what problem it solves, and any technical constraints..."
393
+ onChange={(e) => setCharCount(e.target.value.length)}
394
+ className="glass-input squircle relative flex w-full items-center overflow-hidden rounded-sm border border-stone-300 dark:border-stone-700 bg-white dark:bg-black/80 px-3 py-2 text-sm text-stone-800 dark:text-stone-50 placeholder:text-stone-400 outline-none backdrop-blur-[22px] transition-all duration-300 focus:border-stone-400 dark:focus:border-lime-500 focus:ring-2 focus:ring-offset-2 focus:ring-stone-300/50 dark:focus:ring-lime-500/10"
395
+ />
396
+ </div>
397
+
398
+ {status === 'error' && (
399
+ <p className="text-xs text-red-400">Something went wrong. Please try again.</p>
400
+ )}
401
+
402
+ <button
403
+ type="submit"
404
+ disabled={status === 'submitting' || charCount > 500}
405
+ className="glass-button-white group relative isolate flex w-full cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-4xl! border border-white/30 dark:border-white bg- px-6 py-3 text-sm font-semibold text-stone-100 dark:text-stone-900 bg-stone-950/90 dark:bg-stone-100/80 outline-offset-0 dark:hover:bg-stone-100 hover:shadow-2xl hover:-translate-y-0.5 hover:outline-4 hover:outline-indigo-500 hover:-outline-offset-1 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-300"
406
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
407
+ >
408
+ <span className="relative z-2 flex items-center gap-2">
409
+ {status === 'submitting' ? (
410
+ <>
411
+ <Loader2 size={15} className="animate-spin" />
412
+ Sending...
413
+ </>
414
+ ) : (
415
+ <>
416
+ <Send size={15} />
417
+ Send Project Brief
418
+ </>
419
+ )}
420
+ </span>
421
+ </button>
422
+
423
+ <p className="text-xs text-stone-700 dark:text-stone-400 text-center">
424
+ We'll respond within 1 business day. No spam, ever.
425
+ </p>
426
+ </form>
427
+ )}
428
+ </div>
429
+ </div>
430
+ </div>
431
+ </div>
432
+ </>
433
+ )
434
+ }