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.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/bin/index.js +8 -0
- package/package.json +40 -0
- package/src/cli.js +79 -0
- package/src/commands/add.js +327 -0
- package/src/commands/doctor.js +21 -0
- package/src/commands/list.js +18 -0
- package/src/installers/checkRequirements.js +101 -0
- package/src/installers/copyTemplateFiles.js +79 -0
- package/src/installers/installDependencies.js +69 -0
- package/src/installers/patchPayloadBlocksArray.js +122 -0
- package/src/installers/patchRenderBlocks.js +110 -0
- package/src/installers/runGenerators.js +3 -0
- package/src/installers/writeInstalledBlockReadme.js +117 -0
- package/src/registry/listTemplates.js +17 -0
- package/src/registry/loadTemplateManifest.js +20 -0
- package/src/registry/validateTemplateManifest.js +69 -0
- package/src/utils/logger.js +8 -0
- package/src/utils/packageManager.js +42 -0
- package/src/utils/paths.js +7 -0
- package/src/utils/project.js +122 -0
- package/src/utils/strings.js +7 -0
- package/templates/blocks/DD-BookCall/Component.tsx +573 -0
- package/templates/blocks/DD-BookCall/config.ts +239 -0
- package/templates/blocks/DD-BookCall/manifest.json +112 -0
- package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
- package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
- package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
- package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
- package/templates/blocks/DD-Carousel-A/config.ts +163 -0
- package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
- package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
- package/templates/blocks/DD-Carousel-B/config.ts +176 -0
- package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
- package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
- package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
- package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
- package/templates/blocks/DD-Contact/Component.tsx +439 -0
- package/templates/blocks/DD-Contact/config.ts +217 -0
- package/templates/blocks/DD-Contact/manifest.json +107 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
- package/templates/blocks/DD-FeatCat/config.ts +204 -0
- package/templates/blocks/DD-FeatCat/manifest.json +87 -0
- package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
- package/templates/blocks/DD-FeatStrip/config.ts +134 -0
- package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
- package/templates/blocks/DD-Hero/Component.tsx +317 -0
- package/templates/blocks/DD-Hero/config.ts +267 -0
- package/templates/blocks/DD-Hero/manifest.json +92 -0
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
- package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
- package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
- package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
- package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
- package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
- package/templates/blocks/DD-Pricing/Component.tsx +380 -0
- package/templates/blocks/DD-Pricing/config.ts +215 -0
- package/templates/blocks/DD-Pricing/manifest.json +78 -0
- package/templates/blocks/DD-Process/Component.tsx +155 -0
- package/templates/blocks/DD-Process/config.ts +150 -0
- package/templates/blocks/DD-Process/manifest.json +82 -0
- package/templates/blocks/DD-Section/Component.tsx +327 -0
- package/templates/blocks/DD-Section/config.ts +214 -0
- package/templates/blocks/DD-Section/manifest.json +85 -0
- package/templates/blocks/DD-Services/Component.tsx +188 -0
- package/templates/blocks/DD-Services/config.ts +149 -0
- package/templates/blocks/DD-Services/manifest.json +95 -0
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
- package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
- package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
- package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
- package/templates/blocks/DD-Slider-A/config.ts +118 -0
- package/templates/blocks/DD-Slider-A/manifest.json +86 -0
- package/templates/blocks/DD-StackCards/Component.tsx +496 -0
- package/templates/blocks/DD-StackCards/config.ts +399 -0
- package/templates/blocks/DD-StackCards/manifest.json +87 -0
- package/templates/blocks/DD-Team/Component.tsx +265 -0
- package/templates/blocks/DD-Team/config.ts +173 -0
- package/templates/blocks/DD-Team/manifest.json +82 -0
- package/templates/blocks/DD-TechStack/Component.tsx +189 -0
- package/templates/blocks/DD-TechStack/config.ts +153 -0
- package/templates/blocks/DD-TechStack/manifest.json +118 -0
- package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
- package/templates/blocks/DD-Testimonials/config.ts +120 -0
- package/templates/blocks/DD-Testimonials/manifest.json +87 -0
- package/templates/blocks/DD-Work/Component.tsx +328 -0
- package/templates/blocks/DD-Work/config.ts +165 -0
- package/templates/blocks/DD-Work/manifest.json +87 -0
- package/templates/blocks/DD-Work-B/Component.tsx +320 -0
- package/templates/blocks/DD-Work-B/config.ts +165 -0
- package/templates/blocks/DD-Work-B/manifest.json +75 -0
- package/templates/shared/components/ui/anims/fade.tsx +83 -0
- package/templates/shared/components/ui/button.tsx +233 -0
- package/templates/shared/components/ui/input.tsx +41 -0
- package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
- package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
- package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
- package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
- package/templates/shared/utilities/canUseDOM.ts +1 -0
- 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 & 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
|
+
};
|