11import { useState } from "react" ;
2- import { Check } from "@phosphor-icons/react" ;
32import PlatformNavbar from "@/components/PlatformNavbar" ;
43import Footer from "@/components/Footer" ;
5- import { BackButton } from "@/components/LandingPrimitives" ;
4+ import { BackButton , SectionLabel } from "@/components/LandingPrimitives" ;
65import { useSeo } from "@/hooks/useSeo" ;
76import {
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
1420const 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-
9963const 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