Skip to content

Commit 9526607

Browse files
authored
Merge pull request #97 from fil-one/filipa/fil-849-migrate-waitlist-bucket-intelligence-to-design-tokens
Migrate /waitlist/bucket-intelligence to design tokens + shared form primitives
2 parents 23540d0 + f87a960 commit 9526607

3 files changed

Lines changed: 94 additions & 211 deletions

File tree

scripts/routeMeta.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ export const ROUTE_META = {
534534
"/waitlist/bucket-intelligence": {
535535
title: "Join the Bucket Intelligence Waitlist · Fil One",
536536
description:
537-
"Get early access to Fil One RAG Pipeline — turn any bucket into a queryable knowledge base with semantic search and your own LLM keys.",
537+
"Get early access to Fil One RAG Pipeline. Turn any bucket into a queryable knowledge base with semantic search and your own LLM keys.",
538538
},
539539
"/waitlist/ai-agent-toolkit": {
540540
title: "Join the AI Agent Toolkit Waitlist · Fil One",

src/components/FormControls.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,17 @@ export const CheckboxField = ({
349349
/** Full-width primary submit button with a loading state. */
350350
export const SubmitButton = ({
351351
loading,
352+
disabled,
352353
children,
353354
}: {
354355
loading?: boolean;
356+
/** Extra disabled condition beyond `loading` (e.g. an unfilled required field). */
357+
disabled?: boolean;
355358
children: ReactNode;
356359
}) => (
357360
<button
358361
type="submit"
359-
disabled={loading}
362+
disabled={loading || disabled}
360363
className="btn-primary w-full cursor-pointer border-none disabled:cursor-default disabled:opacity-70"
361364
>
362365
<span className="btn-primary-inner px-6 py-[11px] text-[15px]">

src/pages/BucketIntelligenceWaitlistPage.tsx

Lines changed: 89 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { useState } from "react";
2-
import { Check } from "@phosphor-icons/react";
32
import PlatformNavbar from "@/components/PlatformNavbar";
43
import Footer from "@/components/Footer";
5-
import { BackButton } from "@/components/LandingPrimitives";
4+
import { BackButton, SectionLabel } from "@/components/LandingPrimitives";
65
import { useSeo } from "@/hooks/useSeo";
76
import {
8-
HS_PORTAL_ID,
7+
TextField,
8+
TextAreaField,
9+
SelectField,
10+
SubmitButton,
11+
FormSuccess,
12+
FormError,
13+
} from "@/components/FormControls";
14+
import {
915
HS_BUCKET_INTELLIGENCE_WAITLIST_FORM_GUID,
1016
HS_MARKETING_SUBSCRIPTION_TYPE_ID,
11-
getHubSpotContext,
17+
submitHubSpotForm,
1218
} from "@/lib/hubspot";
1319

1420
const USE_CASES = [
@@ -54,52 +60,10 @@ const STORAGE_AMOUNTS = [
5460
"More than 1 PB",
5561
];
5662

57-
const inputStyle: React.CSSProperties = {
58-
width: "100%",
59-
border: "1px solid rgba(0,0,0,0.10)",
60-
borderRadius: 10,
61-
padding: "10px 14px",
62-
fontFamily: "'Funnel Sans', sans-serif",
63-
fontWeight: 400,
64-
fontSize: 14.5,
65-
color: "#09090B",
66-
backgroundColor: "#FFFFFF",
67-
outline: "none",
68-
transition: "border-color 150ms ease",
69-
};
70-
71-
const selectStyle: React.CSSProperties = {
72-
...inputStyle,
73-
appearance: "none",
74-
WebkitAppearance: "none",
75-
backgroundImage: `url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2352525B' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")`,
76-
backgroundRepeat: "no-repeat",
77-
backgroundPosition: "right 14px center",
78-
paddingRight: 36,
79-
cursor: "pointer",
80-
};
81-
82-
const labelStyle: React.CSSProperties = {
83-
fontFamily: "'Funnel Sans', sans-serif",
84-
fontWeight: 500,
85-
fontSize: 13.5,
86-
color: "#3F3F46",
87-
};
88-
89-
const Field = ({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) => (
90-
<label className="flex flex-col gap-1.5">
91-
<span style={labelStyle}>
92-
{label}
93-
{required && <span aria-hidden="true" style={{ color: "#DC2626", marginLeft: 2 }}>*</span>}
94-
</span>
95-
{children}
96-
</label>
97-
);
98-
9963
const BucketIntelligenceWaitlistPage = () => {
10064
useSeo({
10165
title: "Join the Bucket Intelligence Waitlist · Fil One",
102-
description: "Get early access to Bucket Intelligence. Turn any bucket into a queryable knowledge base — join the waitlist and we'll reach out when your spot is ready.",
66+
description: "Get early access to Bucket Intelligence. Turn any bucket into a queryable knowledge base. Join the waitlist and we'll reach out when your spot is ready.",
10367
canonical: "https://www.fil.one/waitlist/bucket-intelligence",
10468
});
10569

@@ -121,56 +85,43 @@ const BucketIntelligenceWaitlistPage = () => {
12185
setLoading(true);
12286
setError(null);
12387

124-
try {
125-
const res = await fetch(
126-
`https://api.hsforms.com/submissions/v3/integration/submit/${HS_PORTAL_ID}/${HS_BUCKET_INTELLIGENCE_WAITLIST_FORM_GUID}`,
127-
{
128-
method: "POST",
129-
headers: { "Content-Type": "application/json" },
130-
body: JSON.stringify({
131-
fields: [
132-
{ objectTypeId: "0-1", name: "email", value: email },
133-
{ objectTypeId: "0-1", name: "firstname", value: firstName },
134-
{ objectTypeId: "0-1", name: "lastname", value: lastName },
135-
...(useCase ? [{ objectTypeId: "0-1", name: "primary_use_case", value: useCase }] : []),
136-
...(ragSolution ? [{ objectTypeId: "0-1", name: "how_are_you_handling_rag_today", value: ragSolution }] : []),
137-
...(timeline ? [{ objectTypeId: "0-1", name: "timeline", value: timeline }] : []),
138-
...(teamSize ? [{ objectTypeId: "0-1", name: "team_size", value: teamSize }] : []),
139-
...(storageAmount ? [{ objectTypeId: "0-1", name: "amount_of_storage_rag", value: storageAmount }] : []),
140-
...(notes ? [{ objectTypeId: "0-1", name: "message", value: notes }] : []),
141-
],
142-
context: getHubSpotContext("Bucket Intelligence Waitlist"),
143-
legalConsentOptions: {
144-
consent: {
145-
consentToProcess: true,
146-
text: "By joining the waitlist, you consent to allow Fil One to store and process your information and send you product updates.",
147-
communications: [{
148-
value: true,
149-
subscriptionTypeId: HS_MARKETING_SUBSCRIPTION_TYPE_ID,
150-
text: "I agree to receive product updates from Fil One.",
151-
}],
152-
},
153-
},
154-
}),
155-
}
156-
);
157-
if (!res.ok) {
158-
const body = await res.json().catch(() => ({}));
159-
const msg = body?.errors?.map((e: { message: string }) => e.message).join(" | ") || body?.message || JSON.stringify(body);
160-
console.error("HubSpot submission error:", body);
161-
setError(msg);
162-
return;
163-
}
164-
setSubmitted(true);
165-
} catch {
166-
setError("Network error — please check your connection and try again.");
167-
} finally {
168-
setLoading(false);
88+
const result = await submitHubSpotForm({
89+
formGuid: HS_BUCKET_INTELLIGENCE_WAITLIST_FORM_GUID,
90+
pageName: "Bucket Intelligence Waitlist",
91+
fields: [
92+
{ objectTypeId: "0-1", name: "email", value: email },
93+
{ objectTypeId: "0-1", name: "firstname", value: firstName },
94+
{ objectTypeId: "0-1", name: "lastname", value: lastName },
95+
...(useCase ? [{ objectTypeId: "0-1", name: "primary_use_case", value: useCase }] : []),
96+
...(ragSolution ? [{ objectTypeId: "0-1", name: "how_are_you_handling_rag_today", value: ragSolution }] : []),
97+
...(timeline ? [{ objectTypeId: "0-1", name: "timeline", value: timeline }] : []),
98+
...(teamSize ? [{ objectTypeId: "0-1", name: "team_size", value: teamSize }] : []),
99+
...(storageAmount ? [{ objectTypeId: "0-1", name: "amount_of_storage_rag", value: storageAmount }] : []),
100+
...(notes ? [{ objectTypeId: "0-1", name: "message", value: notes }] : []),
101+
],
102+
legalConsentOptions: {
103+
consent: {
104+
consentToProcess: true,
105+
text: "By joining the waitlist, you consent to allow Fil One to store and process your information and send you product updates.",
106+
communications: [{
107+
value: true,
108+
subscriptionTypeId: HS_MARKETING_SUBSCRIPTION_TYPE_ID,
109+
text: "I agree to receive product updates from Fil One.",
110+
}],
111+
},
112+
},
113+
});
114+
115+
setLoading(false);
116+
if (!result.ok) {
117+
setError(result.error);
118+
return;
169119
}
120+
setSubmitted(true);
170121
};
171122

172123
return (
173-
<div className="min-h-screen overflow-x-hidden" style={{ backgroundColor: "#FFFFFF" }}>
124+
<div className="min-h-screen overflow-x-hidden bg-white">
174125
<PlatformNavbar />
175126

176127
<main id="main-content" className="flex flex-col items-center px-5 md:px-8 pt-36 pb-24 w-full">
@@ -180,146 +131,75 @@ const BucketIntelligenceWaitlistPage = () => {
180131

181132
{/* Header */}
182133
<div className="flex flex-col gap-3">
183-
<p style={{ fontFamily: "'DM Mono', monospace", fontWeight: 500, fontSize: 11.5, letterSpacing: "0.08em", color: "#71717A", textTransform: "uppercase" }}>
184-
Early access · Bucket Intelligence
185-
</p>
186-
<h1
187-
className="text-[28px] md:text-[36px]"
188-
style={{ fontFamily: "'Aspekta', sans-serif", fontWeight: 500, lineHeight: "1.15", letterSpacing: "-0.02em", color: "#09090B" }}
189-
>
134+
<SectionLabel>Early access · Bucket Intelligence</SectionLabel>
135+
<h1 className="m-0 font-display font-medium text-[28px] md:text-[36px] leading-[1.15] tracking-[-0.02em] text-zinc-950">
190136
Join the waitlist
191137
</h1>
192-
<p style={{ fontFamily: "'Funnel Sans', sans-serif", fontWeight: 400, fontSize: 15, lineHeight: "1.6", color: "#71717A" }}>
193-
Get early access to Bucket Intelligence — turn any bucket into a queryable knowledge base. We'll reach out as soon as your spot is ready.
138+
<p className="m-0 font-sans font-normal text-[15px] leading-[1.6] text-zinc-500">
139+
Get early access to Bucket Intelligence. Turn any bucket into a queryable knowledge base, and we'll reach out as soon as your spot is ready.
194140
</p>
195141
</div>
196142

197-
<div className="w-full" style={{ height: 1, backgroundColor: "rgba(0,0,0,0.07)" }} />
143+
<div className="h-px w-full bg-black/[0.07]" />
198144

199145
{submitted ? (
200-
<div role="status" className="flex flex-col gap-3 py-6">
201-
<div className="flex items-center justify-center w-10 h-10 rounded-full" style={{ backgroundColor: "#EFF8FF" }}>
202-
<Check size={18} color="#0090FF" />
203-
</div>
204-
<p style={{ fontFamily: "'Aspekta', sans-serif", fontWeight: 500, fontSize: 20, color: "#09090B", letterSpacing: "-0.01em" }}>
205-
You're on the list!
206-
</p>
207-
<p style={{ fontFamily: "'Funnel Sans', sans-serif", fontWeight: 400, fontSize: 14.5, color: "#71717A", lineHeight: "1.6" }}>
208-
We'll reach out as soon as your spot is ready. Keep an eye on your inbox.
209-
</p>
210-
</div>
146+
<FormSuccess title="You're on the list!">
147+
We'll reach out as soon as your spot is ready. Keep an eye on your inbox.
148+
</FormSuccess>
211149
) : (
212150
<form onSubmit={handleSubmit} data-hs-do-not-collect="true" className="flex flex-col gap-5">
213151

214152
<div className="grid grid-cols-2 gap-4">
215-
<Field label="First name" required>
216-
<input
217-
type="text"
218-
value={firstName}
219-
onChange={e => setFirstName(e.target.value)}
220-
placeholder="Jane"
221-
required
222-
style={inputStyle}
223-
onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")}
224-
onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}
225-
/>
226-
</Field>
227-
<Field label="Last name" required>
228-
<input
229-
type="text"
230-
value={lastName}
231-
onChange={e => setLastName(e.target.value)}
232-
placeholder="Smith"
233-
required
234-
style={inputStyle}
235-
onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")}
236-
onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}
237-
/>
238-
</Field>
153+
<TextField label="First name" required type="text" value={firstName} onChange={e => setFirstName(e.target.value)} placeholder="Jane" />
154+
<TextField label="Last name" required type="text" value={lastName} onChange={e => setLastName(e.target.value)} placeholder="Smith" />
239155
</div>
240156

241-
<Field label="Work email" required>
242-
<input
243-
type="email"
244-
value={email}
245-
onChange={e => setEmail(e.target.value)}
246-
placeholder="you@company.com"
247-
required
248-
style={inputStyle}
249-
onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")}
250-
onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}
251-
/>
252-
</Field>
157+
<TextField label="Work email" required type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="you@company.com" />
253158

254159
<div className="grid grid-cols-2 gap-4">
255-
<Field label="Primary use case">
256-
<select value={useCase} onChange={e => setUseCase(e.target.value)} style={selectStyle} onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")} onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}>
257-
<option value="">Select…</option>
258-
{USE_CASES.map(o => <option key={o} value={o}>{o}</option>)}
259-
</select>
260-
</Field>
261-
<Field label="How are you handling RAG today?">
262-
<select value={ragSolution} onChange={e => setRagSolution(e.target.value)} style={selectStyle} onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")} onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}>
263-
<option value="">Select…</option>
264-
{RAG_SOLUTIONS.map(o => <option key={o} value={o}>{o}</option>)}
265-
</select>
266-
</Field>
267-
<Field label="Timeline">
268-
<select value={timeline} onChange={e => setTimeline(e.target.value)} style={selectStyle} onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")} onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}>
269-
<option value="">Select…</option>
270-
{TIMELINES.map(o => <option key={o} value={o}>{o}</option>)}
271-
</select>
272-
</Field>
273-
<Field label="Team size">
274-
<select value={teamSize} onChange={e => setTeamSize(e.target.value)} style={selectStyle} onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")} onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}>
275-
<option value="">Select…</option>
276-
{TEAM_SIZES.map(o => <option key={o} value={o}>{o}</option>)}
277-
</select>
278-
</Field>
160+
<SelectField label="Primary use case" value={useCase} onChange={e => setUseCase(e.target.value)}>
161+
<option value="">Select…</option>
162+
{USE_CASES.map(o => <option key={o} value={o}>{o}</option>)}
163+
</SelectField>
164+
<SelectField label="How are you handling RAG today?" value={ragSolution} onChange={e => setRagSolution(e.target.value)}>
165+
<option value="">Select…</option>
166+
{RAG_SOLUTIONS.map(o => <option key={o} value={o}>{o}</option>)}
167+
</SelectField>
168+
<SelectField label="Timeline" value={timeline} onChange={e => setTimeline(e.target.value)}>
169+
<option value="">Select…</option>
170+
{TIMELINES.map(o => <option key={o} value={o}>{o}</option>)}
171+
</SelectField>
172+
<SelectField label="Team size" value={teamSize} onChange={e => setTeamSize(e.target.value)}>
173+
<option value="">Select…</option>
174+
{TEAM_SIZES.map(o => <option key={o} value={o}>{o}</option>)}
175+
</SelectField>
279176
</div>
280177

281-
<Field label="Amount of storage">
282-
<select value={storageAmount} onChange={e => setStorageAmount(e.target.value)} style={selectStyle} onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")} onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}>
283-
<option value="">Select…</option>
284-
{STORAGE_AMOUNTS.map(o => <option key={o} value={o}>{o}</option>)}
285-
</select>
286-
</Field>
178+
<SelectField label="Amount of storage" value={storageAmount} onChange={e => setStorageAmount(e.target.value)}>
179+
<option value="">Select…</option>
180+
{STORAGE_AMOUNTS.map(o => <option key={o} value={o}>{o}</option>)}
181+
</SelectField>
287182

288-
<Field label="Notes (optional)">
289-
<textarea
290-
value={notes}
291-
onChange={e => setNotes(e.target.value)}
292-
rows={3}
293-
placeholder="What file types do you work with? How large is your document corpus?"
294-
style={{ ...inputStyle, resize: "vertical", lineHeight: 1.55 }}
295-
onFocus={e => (e.target.style.borderColor = "rgba(0,0,0,0.30)")}
296-
onBlur={e => (e.target.style.borderColor = "rgba(0,0,0,0.10)")}
297-
/>
298-
</Field>
183+
<TextAreaField
184+
label="Notes (optional)"
185+
value={notes}
186+
onChange={e => setNotes(e.target.value)}
187+
rows={3}
188+
placeholder="What file types do you work with? How large is your document corpus?"
189+
/>
299190

300-
<div className="w-full" style={{ height: 1, backgroundColor: "rgba(0,0,0,0.07)" }} />
191+
<div className="h-px w-full bg-black/[0.07]" />
301192

302-
<p style={{ fontFamily: "'Funnel Sans', sans-serif", fontWeight: 400, fontSize: 12.5, lineHeight: "1.7", color: "#71717A" }}>
193+
<p className="font-sans font-normal text-[12.5px] leading-[1.7] text-zinc-500">
303194
By submitting this form you consent to allow Fil One to store and process your information and send you product updates. You can unsubscribe at any time. See our{" "}
304-
<a href="/privacy" style={{ color: "#71717A", textDecoration: "underline" }}>Privacy Policy</a>.
195+
<a href="/privacy" className="text-zinc-500 underline">Privacy Policy</a>.
305196
</p>
306197

307198
<div className="flex flex-col gap-2">
308-
<button
309-
type="submit"
310-
disabled={!email || loading}
311-
className="btn-primary"
312-
style={{ border: "none", cursor: (!email || loading) ? "default" : "pointer", opacity: (!email || loading) ? 0.7 : 1, width: "100%" }}
313-
>
314-
<span className="btn-primary-inner" style={{ padding: "11px 24px", fontSize: 15 }}>
315-
{loading ? "Joining…" : "Join waitlist"}
316-
</span>
317-
</button>
318-
{error && (
319-
<p role="alert" style={{ fontFamily: "'Funnel Sans', sans-serif", fontSize: 13, color: "#DC2626", textAlign: "center" }}>
320-
{error}
321-
</p>
322-
)}
199+
<SubmitButton loading={loading} disabled={!email}>
200+
{loading ? "Joining…" : "Join waitlist"}
201+
</SubmitButton>
202+
{error && <FormError>{error}</FormError>}
323203
</div>
324204

325205
</form>

0 commit comments

Comments
 (0)