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,439 @@
|
|
|
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 = (
|
|
36
|
+
LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>
|
|
37
|
+
)[name];
|
|
38
|
+
if (!Icon) return null;
|
|
39
|
+
return <Icon size={size} {...props} />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DDContact: React.FC<DDContactProps> = ({
|
|
43
|
+
eyebrow,
|
|
44
|
+
headlineMain,
|
|
45
|
+
headlineAccent,
|
|
46
|
+
description,
|
|
47
|
+
infoItems,
|
|
48
|
+
availabilityActive,
|
|
49
|
+
availabilityLabel,
|
|
50
|
+
availabilitySubtext,
|
|
51
|
+
form,
|
|
52
|
+
serviceOptions,
|
|
53
|
+
budgetOptions,
|
|
54
|
+
timelineOptions,
|
|
55
|
+
sectionId,
|
|
56
|
+
customCSS,
|
|
57
|
+
}) => {
|
|
58
|
+
const [status, setStatus] = useState<Status>("idle");
|
|
59
|
+
const [charCount, setCharCount] = useState(0);
|
|
60
|
+
|
|
61
|
+
const formID = typeof form === "object" && form !== null ? form.id : form;
|
|
62
|
+
|
|
63
|
+
const formIdentifier = useMemo(() => {
|
|
64
|
+
const safeSection = (sectionId || "contact")
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9-_]/g, "-");
|
|
67
|
+
|
|
68
|
+
return `studio-contact-${String(formID || "unknown")}-${safeSection}`;
|
|
69
|
+
}, [formID, sectionId]);
|
|
70
|
+
|
|
71
|
+
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
const formElement = e.currentTarget;
|
|
74
|
+
const data = new FormData(formElement);
|
|
75
|
+
|
|
76
|
+
const message = data.get("message") as string;
|
|
77
|
+
if (message && message.length > 500) return;
|
|
78
|
+
|
|
79
|
+
setStatus("submitting");
|
|
80
|
+
|
|
81
|
+
const submissionData = [
|
|
82
|
+
{ field: "name", value: data.get("name") },
|
|
83
|
+
{ field: "email", value: data.get("email") },
|
|
84
|
+
{ field: "company", value: data.get("company") },
|
|
85
|
+
{ field: "service", value: data.get("service") },
|
|
86
|
+
{ field: "budget", value: data.get("budget") },
|
|
87
|
+
{ field: "timeline", value: data.get("timeline") },
|
|
88
|
+
{ field: "message", value: data.get("message") },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (!formID) {
|
|
92
|
+
setStatus("success");
|
|
93
|
+
formElement.reset();
|
|
94
|
+
setCharCount(0);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(`${getClientSideURL()}/api/form-submissions`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
form: formID,
|
|
106
|
+
submissionData,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
setStatus("success");
|
|
112
|
+
formElement.reset();
|
|
113
|
+
setCharCount(0);
|
|
114
|
+
} else {
|
|
115
|
+
setStatus("error");
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
setStatus("error");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
125
|
+
|
|
126
|
+
<div id={sectionId ?? "contact"} className="py-24">
|
|
127
|
+
<div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
|
|
128
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
|
|
129
|
+
<div>
|
|
130
|
+
{eyebrow && (
|
|
131
|
+
<p className="text-[11.5px] font-semibold text-stone-400 tracking-widest uppercase mb-3 top-caption">
|
|
132
|
+
{eyebrow}
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
<h2 className="text-4xl md:text-5xl font-semibold text-stone-900 dark:text-stone-50 leading-tight mb-6">
|
|
137
|
+
{headlineMain}
|
|
138
|
+
<br />
|
|
139
|
+
<span className="text-stone-400">{headlineAccent}</span>
|
|
140
|
+
</h2>
|
|
141
|
+
|
|
142
|
+
{description && (
|
|
143
|
+
<p className="text-stone-700 dark:text-stone-300 text-base leading-relaxed mb-10">
|
|
144
|
+
{description}
|
|
145
|
+
</p>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{infoItems && infoItems.length > 0 && (
|
|
149
|
+
<div className="space-y-4 mb-10">
|
|
150
|
+
{infoItems.map((item, i) => (
|
|
151
|
+
<div key={i} className="flex gap-4">
|
|
152
|
+
<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">
|
|
153
|
+
<DynamicIcon
|
|
154
|
+
className="text-indigo-500 dark:text-lime-500"
|
|
155
|
+
name={item.iconName}
|
|
156
|
+
size={15}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<div>
|
|
160
|
+
<div className="text-sm font-semibold text-stone-800 dark:text-stone-50 mb-0.5">
|
|
161
|
+
{item.title}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="text-xs text-stone-600 dark:text-stone-300">
|
|
164
|
+
{item.description}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{availabilityActive && (
|
|
173
|
+
<div
|
|
174
|
+
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"
|
|
175
|
+
style={{ cornerShape: "squircle" } as React.CSSProperties}
|
|
176
|
+
>
|
|
177
|
+
<div className="w-3.5 h-3.5 rounded-full bg-lime-700 border-2 border-lime-500 animate-pulse shrink-0 shadow-2xl" />
|
|
178
|
+
<div>
|
|
179
|
+
{availabilityLabel && (
|
|
180
|
+
<div className="text-md font-semibold text-lime-500 dark:text-stone-50">
|
|
181
|
+
{availabilityLabel}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{availabilitySubtext && (
|
|
186
|
+
<div className="text-xs font-normal! text-stone-300 pt-1">
|
|
187
|
+
{availabilitySubtext}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<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">
|
|
196
|
+
{status === "success" ? (
|
|
197
|
+
<div className="flex flex-col items-center justify-center text-center py-12">
|
|
198
|
+
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-lime-400/10 border border-emerald-400/30 mb-5">
|
|
199
|
+
<Check size={28} className="text-lime-400" />
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<h3 className="text-xl font-semibold text-stone-900 dark:text-stone-50 mb-2">
|
|
203
|
+
Message sent!
|
|
204
|
+
</h3>
|
|
205
|
+
|
|
206
|
+
<p className="text-sm text-stone-400 max-w-xs">
|
|
207
|
+
Thanks for reaching out. We'll review your project details
|
|
208
|
+
and get back to you within one business day.
|
|
209
|
+
</p>
|
|
210
|
+
|
|
211
|
+
<button
|
|
212
|
+
onClick={() => setStatus("idle")}
|
|
213
|
+
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"
|
|
214
|
+
>
|
|
215
|
+
Send another message
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
) : (
|
|
219
|
+
<form
|
|
220
|
+
id={formIdentifier}
|
|
221
|
+
name={formIdentifier}
|
|
222
|
+
data-form-id={String(formID || "")}
|
|
223
|
+
onSubmit={handleSubmit}
|
|
224
|
+
className="space-y-5"
|
|
225
|
+
>
|
|
226
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
227
|
+
<div>
|
|
228
|
+
<label
|
|
229
|
+
htmlFor={`${formIdentifier}-name`}
|
|
230
|
+
className="block text-xs font-medium text-stone-400 mb-1.5"
|
|
231
|
+
>
|
|
232
|
+
Your Name *
|
|
233
|
+
</label>
|
|
234
|
+
|
|
235
|
+
<Input
|
|
236
|
+
id={`${formIdentifier}-name`}
|
|
237
|
+
type="text"
|
|
238
|
+
name="name"
|
|
239
|
+
autoComplete="name"
|
|
240
|
+
required
|
|
241
|
+
placeholder="Marcus Holt"
|
|
242
|
+
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"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<div>
|
|
247
|
+
<label
|
|
248
|
+
htmlFor={`${formIdentifier}-email`}
|
|
249
|
+
className="block text-xs font-medium text-stone-400 mb-1.5"
|
|
250
|
+
>
|
|
251
|
+
Email Address *
|
|
252
|
+
</label>
|
|
253
|
+
|
|
254
|
+
<Input
|
|
255
|
+
id={`${formIdentifier}-email`}
|
|
256
|
+
type="email"
|
|
257
|
+
name="email"
|
|
258
|
+
autoComplete="email"
|
|
259
|
+
required
|
|
260
|
+
placeholder="you@company.com"
|
|
261
|
+
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"
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div>
|
|
267
|
+
<label
|
|
268
|
+
htmlFor={`${formIdentifier}-company`}
|
|
269
|
+
className="block text-xs font-medium text-stone-400 mb-1.5"
|
|
270
|
+
>
|
|
271
|
+
Company / Project Name
|
|
272
|
+
</label>
|
|
273
|
+
|
|
274
|
+
<Input
|
|
275
|
+
id={`${formIdentifier}-company`}
|
|
276
|
+
type="text"
|
|
277
|
+
name="company"
|
|
278
|
+
autoComplete="organization"
|
|
279
|
+
placeholder="Acme Inc."
|
|
280
|
+
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"
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{serviceOptions && serviceOptions.length > 0 && (
|
|
285
|
+
<div>
|
|
286
|
+
<label
|
|
287
|
+
htmlFor={`${formIdentifier}-service`}
|
|
288
|
+
className="block text-xs font-medium text-stone-400 mb-1.5"
|
|
289
|
+
>
|
|
290
|
+
Service Needed *
|
|
291
|
+
</label>
|
|
292
|
+
|
|
293
|
+
<select
|
|
294
|
+
id={`${formIdentifier}-service`}
|
|
295
|
+
name="service"
|
|
296
|
+
autoComplete="off"
|
|
297
|
+
required
|
|
298
|
+
defaultValue=""
|
|
299
|
+
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"
|
|
300
|
+
>
|
|
301
|
+
<option value="" disabled>
|
|
302
|
+
Select a service...
|
|
303
|
+
</option>
|
|
304
|
+
|
|
305
|
+
{serviceOptions.map((s, i) => (
|
|
306
|
+
<option key={i} value={s.label}>
|
|
307
|
+
{s.label}
|
|
308
|
+
</option>
|
|
309
|
+
))}
|
|
310
|
+
</select>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
315
|
+
{budgetOptions && budgetOptions.length > 0 && (
|
|
316
|
+
<div>
|
|
317
|
+
<label
|
|
318
|
+
htmlFor={`${formIdentifier}-budget`}
|
|
319
|
+
className="block text-xs font-medium text-stone-400 mb-1.5"
|
|
320
|
+
>
|
|
321
|
+
Budget Range *
|
|
322
|
+
</label>
|
|
323
|
+
|
|
324
|
+
<select
|
|
325
|
+
id={`${formIdentifier}-budget`}
|
|
326
|
+
name="budget"
|
|
327
|
+
autoComplete="off"
|
|
328
|
+
required
|
|
329
|
+
defaultValue=""
|
|
330
|
+
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"
|
|
331
|
+
>
|
|
332
|
+
<option value="" disabled>
|
|
333
|
+
Select budget...
|
|
334
|
+
</option>
|
|
335
|
+
|
|
336
|
+
{budgetOptions.map((b, i) => (
|
|
337
|
+
<option key={i} value={b.label}>
|
|
338
|
+
{b.label}
|
|
339
|
+
</option>
|
|
340
|
+
))}
|
|
341
|
+
</select>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{timelineOptions && timelineOptions.length > 0 && (
|
|
346
|
+
<div>
|
|
347
|
+
<label
|
|
348
|
+
htmlFor={`${formIdentifier}-timeline`}
|
|
349
|
+
className="block text-xs font-medium text-stone-400 mb-1.5"
|
|
350
|
+
>
|
|
351
|
+
Timeline
|
|
352
|
+
</label>
|
|
353
|
+
|
|
354
|
+
<select
|
|
355
|
+
id={`${formIdentifier}-timeline`}
|
|
356
|
+
name="timeline"
|
|
357
|
+
autoComplete="off"
|
|
358
|
+
defaultValue=""
|
|
359
|
+
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"
|
|
360
|
+
>
|
|
361
|
+
<option value="" disabled>
|
|
362
|
+
Select timeline...
|
|
363
|
+
</option>
|
|
364
|
+
|
|
365
|
+
{timelineOptions.map((t, i) => (
|
|
366
|
+
<option key={i} value={t.label}>
|
|
367
|
+
{t.label}
|
|
368
|
+
</option>
|
|
369
|
+
))}
|
|
370
|
+
</select>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div>
|
|
376
|
+
<label
|
|
377
|
+
htmlFor={`${formIdentifier}-message`}
|
|
378
|
+
className="block text-xs font-medium text-stone-400 mb-1.5"
|
|
379
|
+
>
|
|
380
|
+
Project Description *
|
|
381
|
+
<span
|
|
382
|
+
className={`ml-2 font-normal ${charCount > 500 ? "text-red-400" : "text-stone-400"}`}
|
|
383
|
+
>
|
|
384
|
+
{charCount}/500
|
|
385
|
+
</span>
|
|
386
|
+
</label>
|
|
387
|
+
|
|
388
|
+
<textarea
|
|
389
|
+
id={`${formIdentifier}-message`}
|
|
390
|
+
name="message"
|
|
391
|
+
autoComplete="off"
|
|
392
|
+
required
|
|
393
|
+
rows={4}
|
|
394
|
+
maxLength={500}
|
|
395
|
+
placeholder="Tell us about your project — what you're building, what problem it solves, and any technical constraints..."
|
|
396
|
+
onChange={(e) => setCharCount(e.target.value.length)}
|
|
397
|
+
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"
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{status === "error" && (
|
|
402
|
+
<p className="text-xs text-red-400">
|
|
403
|
+
Something went wrong. Please try again.
|
|
404
|
+
</p>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
<button
|
|
408
|
+
type="submit"
|
|
409
|
+
disabled={status === "submitting" || charCount > 500}
|
|
410
|
+
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"
|
|
411
|
+
style={{ cornerShape: "squircle" } as React.CSSProperties}
|
|
412
|
+
>
|
|
413
|
+
<span className="relative z-2 flex items-center gap-2">
|
|
414
|
+
{status === "submitting" ? (
|
|
415
|
+
<>
|
|
416
|
+
<Loader2 size={15} className="animate-spin" />
|
|
417
|
+
Sending...
|
|
418
|
+
</>
|
|
419
|
+
) : (
|
|
420
|
+
<>
|
|
421
|
+
<Send size={15} />
|
|
422
|
+
Send Project Brief
|
|
423
|
+
</>
|
|
424
|
+
)}
|
|
425
|
+
</span>
|
|
426
|
+
</button>
|
|
427
|
+
|
|
428
|
+
<p className="text-xs text-stone-700 dark:text-stone-400 text-center">
|
|
429
|
+
We'll respond within 1 business day. No spam, ever.
|
|
430
|
+
</p>
|
|
431
|
+
</form>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</>
|
|
438
|
+
);
|
|
439
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { Block } from 'payload'
|
|
2
|
+
|
|
3
|
+
export const DDContact: Block = {
|
|
4
|
+
slug: 'DD-Contact',
|
|
5
|
+
interfaceName: 'DDContact',
|
|
6
|
+
labels: {
|
|
7
|
+
singular: 'Studio Contact',
|
|
8
|
+
plural: 'Studio Contact Sections'
|
|
9
|
+
},
|
|
10
|
+
fields: [
|
|
11
|
+
{
|
|
12
|
+
type: 'tabs',
|
|
13
|
+
tabs: [
|
|
14
|
+
{
|
|
15
|
+
label: 'Info',
|
|
16
|
+
fields: [
|
|
17
|
+
{
|
|
18
|
+
name: 'eyebrow',
|
|
19
|
+
type: 'text',
|
|
20
|
+
label: 'Eyebrow Label',
|
|
21
|
+
defaultValue: 'Start a Project'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'headlineMain',
|
|
25
|
+
type: 'text',
|
|
26
|
+
label: 'Headline (Main)',
|
|
27
|
+
required: true,
|
|
28
|
+
defaultValue: "Let's build"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'headlineAccent',
|
|
32
|
+
type: 'text',
|
|
33
|
+
label: 'Headline (Accent)',
|
|
34
|
+
defaultValue: 'something great'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'description',
|
|
38
|
+
type: 'textarea',
|
|
39
|
+
label: 'Description',
|
|
40
|
+
defaultValue:
|
|
41
|
+
"Tell us about your project and we'll get back to you within one business day. We take on 3–4 projects per quarter, so spots are limited."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'infoItems',
|
|
45
|
+
type: 'array',
|
|
46
|
+
label: 'Info Items',
|
|
47
|
+
admin: {
|
|
48
|
+
description: 'Bullet points shown on the left column'
|
|
49
|
+
},
|
|
50
|
+
fields: [
|
|
51
|
+
{
|
|
52
|
+
name: 'iconName',
|
|
53
|
+
type: 'text',
|
|
54
|
+
label: 'Lucide Icon Name',
|
|
55
|
+
admin: {
|
|
56
|
+
description: 'e.g. Clock, DollarSign, FileText, Lock'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'title',
|
|
61
|
+
type: 'text',
|
|
62
|
+
label: 'Title',
|
|
63
|
+
required: true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'description',
|
|
67
|
+
type: 'text',
|
|
68
|
+
label: 'Description',
|
|
69
|
+
required: true
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
defaultValue: [
|
|
73
|
+
{
|
|
74
|
+
iconName: 'Clock',
|
|
75
|
+
title: 'Response within 24 hours',
|
|
76
|
+
description: "We reply to every inquiry, even if we're not the right fit."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
iconName: 'DollarSign',
|
|
80
|
+
title: 'Free discovery call',
|
|
81
|
+
description: '30-minute call to understand your project before any commitment.'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
iconName: 'FileText',
|
|
85
|
+
title: 'Detailed proposal',
|
|
86
|
+
description: 'Fixed-price proposal with scope, timeline, and deliverables.'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
iconName: 'Lock',
|
|
90
|
+
title: 'NDA available',
|
|
91
|
+
description: 'Happy to sign an NDA before discussing sensitive details.'
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'availabilityActive',
|
|
97
|
+
type: 'checkbox',
|
|
98
|
+
label: 'Show Availability Badge',
|
|
99
|
+
defaultValue: true
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'availabilityLabel',
|
|
103
|
+
type: 'text',
|
|
104
|
+
label: 'Availability Label',
|
|
105
|
+
defaultValue: 'Currently accepting projects',
|
|
106
|
+
admin: {
|
|
107
|
+
condition: (_, s) => s?.availabilityActive
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'availabilitySubtext',
|
|
112
|
+
type: 'text',
|
|
113
|
+
label: 'Availability Sub-text',
|
|
114
|
+
defaultValue: '2 spots remaining for Q2 2026',
|
|
115
|
+
admin: {
|
|
116
|
+
condition: (_, s) => s?.availabilityActive
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: 'Form',
|
|
123
|
+
fields: [
|
|
124
|
+
// Reconnect option for projects using Payload Form Builder or a compatible forms collection:
|
|
125
|
+
// {
|
|
126
|
+
// name: 'form',
|
|
127
|
+
// type: 'relationship',
|
|
128
|
+
// relationTo: 'forms',
|
|
129
|
+
// label: 'Payload Form',
|
|
130
|
+
// admin: {
|
|
131
|
+
// description: 'Select the form used to store Studio Contact submissions.'
|
|
132
|
+
// }
|
|
133
|
+
// },
|
|
134
|
+
{
|
|
135
|
+
name: 'serviceOptions',
|
|
136
|
+
type: 'array',
|
|
137
|
+
label: 'Service Options',
|
|
138
|
+
fields: [
|
|
139
|
+
{
|
|
140
|
+
name: 'label',
|
|
141
|
+
type: 'text',
|
|
142
|
+
label: 'Option Label',
|
|
143
|
+
required: true
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
defaultValue: [
|
|
147
|
+
{ label: 'UX/UI Design' },
|
|
148
|
+
{ label: 'Web Application Development' },
|
|
149
|
+
{ label: 'CMS Architecture' },
|
|
150
|
+
{ label: 'Digital Product Consulting' },
|
|
151
|
+
{ label: 'Performance & Optimization' },
|
|
152
|
+
{ label: 'Legacy Modernization' }
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'budgetOptions',
|
|
157
|
+
type: 'array',
|
|
158
|
+
label: 'Budget Options',
|
|
159
|
+
fields: [
|
|
160
|
+
{
|
|
161
|
+
name: 'label',
|
|
162
|
+
type: 'text',
|
|
163
|
+
label: 'Option Label',
|
|
164
|
+
required: true
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
defaultValue: [
|
|
168
|
+
{ label: 'Under $5k' },
|
|
169
|
+
{ label: '$5k – $15k' },
|
|
170
|
+
{ label: '$15k – $30k' },
|
|
171
|
+
{ label: '$30k – $60k' },
|
|
172
|
+
{ label: '$60k+' }
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'timelineOptions',
|
|
177
|
+
type: 'array',
|
|
178
|
+
label: 'Timeline Options',
|
|
179
|
+
fields: [
|
|
180
|
+
{
|
|
181
|
+
name: 'label',
|
|
182
|
+
type: 'text',
|
|
183
|
+
label: 'Option Label',
|
|
184
|
+
required: true
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
defaultValue: [
|
|
188
|
+
{ label: 'ASAP' },
|
|
189
|
+
{ label: '1–2 months' },
|
|
190
|
+
{ label: '2–4 months' },
|
|
191
|
+
{ label: '4–6 months' },
|
|
192
|
+
{ label: 'Flexible' }
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
label: 'Settings',
|
|
199
|
+
fields: [
|
|
200
|
+
{
|
|
201
|
+
name: 'sectionId',
|
|
202
|
+
type: 'text',
|
|
203
|
+
label: 'Section ID',
|
|
204
|
+
defaultValue: 'contact'
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'customCSS',
|
|
208
|
+
type: 'code',
|
|
209
|
+
label: 'Custom CSS',
|
|
210
|
+
admin: { language: 'css' }
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|