blocks-dusted 0.1.0

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