11import { conform , useForm } from "@conform-to/react" ;
22import { parse } from "@conform-to/zod" ;
3- import { ArrowRightIcon , EnvelopeIcon , HeartIcon , UserIcon } from "@heroicons/react/20/solid" ;
3+ import { ArrowRightIcon , EnvelopeIcon , UserIcon } from "@heroicons/react/20/solid" ;
44import { HandRaisedIcon } from "@heroicons/react/24/solid" ;
5- import { ActionFunction , json } from "@remix-run/node" ;
5+ import { RadioGroup } from "@radix-ui/react-radio-group" ;
6+ import { json , type ActionFunction } from "@remix-run/node" ;
67import { Form , useActionData } from "@remix-run/react" ;
78import { motion } from "framer-motion" ;
8- import { forwardRef , useState } from "react" ;
9+ import { forwardRef , useEffect , useState } from "react" ;
910import { z } from "zod" ;
1011import { AppContainer , MainCenteredContainer } from "~/components/layout/AppLayout" ;
1112import { BackgroundWrapper } from "~/components/BackgroundWrapper" ;
@@ -18,6 +19,8 @@ import { Hint } from "~/components/primitives/Hint";
1819import { Input } from "~/components/primitives/Input" ;
1920import { InputGroup } from "~/components/primitives/InputGroup" ;
2021import { Label } from "~/components/primitives/Label" ;
22+ import { RadioGroupItem } from "~/components/primitives/RadioButton" ;
23+ import { Select , SelectItem } from "~/components/primitives/Select" ;
2124import { prisma } from "~/db.server" ;
2225import { useFeatures } from "~/hooks/useFeatures" ;
2326import { useUser } from "~/hooks/useUser" ;
@@ -27,6 +30,40 @@ import { requireUserId } from "~/services/session.server";
2730import { rootPath } from "~/utils/pathBuilder" ;
2831import { getVercelInstallParams } from "~/v3/vercel" ;
2932
33+ const referralSourceOptions = [
34+ "Search engine" ,
35+ "YouTube" ,
36+ "Twitter/X" ,
37+ "LinkedIn" ,
38+ "Word of mouth" ,
39+ "AI assistant/LLM" ,
40+ "Blog/article" ,
41+ "Event" ,
42+ "Other" ,
43+ ] as const ;
44+
45+ const roleOptions = [
46+ "Founder" ,
47+ "Staff/principal engineer" ,
48+ "Senior software engineer" ,
49+ "Software engineer" ,
50+ "AI/ML engineer" ,
51+ "Engineering manager" ,
52+ "Product engineer" ,
53+ "Non technical builder using AI tools" ,
54+ "Student/learner" ,
55+ "Other" ,
56+ ] as const ;
57+
58+ function shuffleArray < T > ( arr : T [ ] ) : T [ ] {
59+ const shuffled = [ ...arr ] ;
60+ for ( let i = shuffled . length - 1 ; i > 0 ; i -- ) {
61+ const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
62+ [ shuffled [ i ] , shuffled [ j ] ] = [ shuffled [ j ] , shuffled [ i ] ] ;
63+ }
64+ return shuffled ;
65+ }
66+
3067function createSchema (
3168 constraints : {
3269 isEmailUnique ?: ( email : string ) => Promise < boolean > ;
@@ -40,13 +77,11 @@ function createSchema(
4077 . email ( )
4178 . superRefine ( ( email , ctx ) => {
4279 if ( constraints . isEmailUnique === undefined ) {
43- //client-side validation skips this
4480 ctx . addIssue ( {
4581 code : z . ZodIssueCode . custom ,
4682 message : conform . VALIDATION_UNDEFINED ,
4783 } ) ;
4884 } else {
49- // Tell zod this is an async validation by returning the promise
5085 return constraints . isEmailUnique ( email ) . then ( ( isUnique ) => {
5186 if ( isUnique ) {
5287 return ;
@@ -61,6 +96,9 @@ function createSchema(
6196 } ) ,
6297 confirmEmail : z . string ( ) ,
6398 referralSource : z . string ( ) . optional ( ) ,
99+ referralSourceOther : z . string ( ) . optional ( ) ,
100+ role : z . string ( ) . optional ( ) ,
101+ roleOther : z . string ( ) . optional ( ) ,
64102 } )
65103 . refine ( ( value ) => value . email === value . confirmEmail , {
66104 message : "Emails must match" ,
@@ -99,19 +137,39 @@ export const action: ActionFunction = async ({ request }) => {
99137 }
100138
101139 try {
102- const updatedUser = await updateUser ( {
140+ const onboardingData : Record < string , string | undefined > = { } ;
141+
142+ if ( submission . value . referralSource ) {
143+ onboardingData . referralSource = submission . value . referralSource ;
144+ if ( submission . value . referralSource === "Other" && submission . value . referralSourceOther ) {
145+ onboardingData . referralSourceOther = submission . value . referralSourceOther ;
146+ }
147+ }
148+
149+ if ( submission . value . role ) {
150+ onboardingData . role = submission . value . role ;
151+ if ( submission . value . role === "Other" && submission . value . roleOther ) {
152+ onboardingData . roleOther = submission . value . roleOther ;
153+ }
154+ }
155+
156+ const referralSourceForLegacy =
157+ submission . value . referralSource === "Other" && submission . value . referralSourceOther
158+ ? `Other: ${ submission . value . referralSourceOther } `
159+ : submission . value . referralSource ;
160+
161+ await updateUser ( {
103162 id : userId ,
104163 name : submission . value . name ,
105164 email : submission . value . email ,
106- referralSource : submission . value . referralSource ,
165+ referralSource : referralSourceForLegacy ,
166+ onboardingData,
107167 } ) ;
108168
109- // Preserve Vercel integration params if present
110169 const vercelParams = getVercelInstallParams ( request ) ;
111170 let redirectUrl = rootPath ( ) ;
112171
113172 if ( vercelParams ) {
114- // Redirect to orgs/new with params preserved
115173 const params = new URLSearchParams ( {
116174 code : vercelParams . code ,
117175 configurationId : vercelParams . configurationId ,
@@ -143,10 +201,24 @@ export default function Page() {
143201 const lastSubmission = useActionData ( ) ;
144202 const [ enteredEmail , setEnteredEmail ] = useState < string > ( user . email ?? "" ) ;
145203 const { isManagedCloud } = useFeatures ( ) ;
204+ const [ selectedReferralSource , setSelectedReferralSource ] = useState < string | undefined > ( ) ;
205+ const [ selectedRole , setSelectedRole ] = useState < string > ( "" ) ;
146206
147- const [ form , { name, email, confirmEmail, referralSource } ] = useForm ( {
207+ const [ shuffledReferralSources , setShuffledReferralSources ] = useState < string [ ] > ( [
208+ ...referralSourceOptions ,
209+ ] ) ;
210+ const [ shuffledRoles , setShuffledRoles ] = useState < string [ ] > ( [ ...roleOptions ] ) ;
211+
212+ useEffect ( ( ) => {
213+ const nonOtherReferral = referralSourceOptions . filter ( ( r ) => r !== "Other" ) ;
214+ setShuffledReferralSources ( [ ...shuffleArray ( nonOtherReferral ) , "Other" ] ) ;
215+
216+ const nonOtherRoles = roleOptions . filter ( ( r ) => r !== "Other" ) ;
217+ setShuffledRoles ( [ ...shuffleArray ( nonOtherRoles ) , "Other" ] ) ;
218+ } , [ ] ) ;
219+
220+ const [ form , { name, email, confirmEmail } ] = useForm ( {
148221 id : "confirm-basic-details" ,
149- // TODO: type this
150222 lastSubmission : lastSubmission as any ,
151223 onValidate ( { formData } ) {
152224 return parse ( formData , { schema : createSchema ( ) } ) ;
@@ -159,7 +231,7 @@ export default function Page() {
159231 return (
160232 < AppContainer className = "bg-charcoal-900" >
161233 < BackgroundWrapper >
162- < MainCenteredContainer className = "max-w-[26rem ] rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg" >
234+ < MainCenteredContainer className = "max-w-[29rem ] rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg" >
163235 < Form method = "post" { ...form . props } >
164236 < FormTitle
165237 title = "Welcome to Trigger.dev"
@@ -187,19 +259,22 @@ export default function Page() {
187259 />
188260 < Fieldset >
189261 < InputGroup >
190- < Label htmlFor = { name . id } > Full name</ Label >
262+ < Label htmlFor = { name . id } >
263+ Full name < span className = "text-text-bright" > *</ span >
264+ </ Label >
191265 < Input
192266 { ...conform . input ( name , { type : "text" } ) }
193267 defaultValue = { user . name ?? "" }
194268 placeholder = "Your full name"
195269 icon = { UserIcon }
196270 autoFocus
197271 />
198- < Hint > Your team will see this name and we'll use it to contact you.</ Hint >
199272 < FormError id = { name . errorId } > { name . error } </ FormError >
200273 </ InputGroup >
201274 < InputGroup >
202- < Label htmlFor = { email . id } > Email</ Label >
275+ < Label htmlFor = { email . id } >
276+ Email < span className = "text-text-bright" > *</ span >
277+ </ Label >
203278 < Input
204279 { ...conform . input ( email , { type : "email" } ) }
205280 defaultValue = { enteredEmail }
@@ -210,9 +285,6 @@ export default function Page() {
210285 icon = { EnvelopeIcon }
211286 spellCheck = { false }
212287 />
213- { ! shouldShowConfirm && (
214- < Hint > Confirm this is the email you'd like for your Trigger.dev account.</ Hint >
215- ) }
216288 < FormError id = { email . errorId } > { email . error } </ FormError >
217289 </ InputGroup >
218290
@@ -225,26 +297,83 @@ export default function Page() {
225297 icon = { EnvelopeIcon }
226298 spellCheck = { false }
227299 />
228- < Hint >
229- Check this is the email you'd like associated with your Trigger.dev account.
230- </ Hint >
231300 < FormError id = { confirmEmail . errorId } > { confirmEmail . error } </ FormError >
232301 </ InputGroup >
233302 ) : (
234303 < >
235304 < input { ...conform . input ( confirmEmail , { type : "hidden" } ) } value = { user . email } />
236305 </ >
237306 ) }
307+
238308 { isManagedCloud && (
239- < InputGroup >
240- < Label htmlFor = { confirmEmail . id } > How did you hear about us?</ Label >
241- < Input
242- { ...conform . input ( referralSource , { type : "text" } ) }
243- placeholder = "LLM, Google, X (Twitter)…?"
244- icon = { HeartIcon }
245- spellCheck = { false }
246- />
247- </ InputGroup >
309+ < >
310+ < div className = "border-t border-charcoal-700" />
311+ < InputGroup >
312+ < Label className = "mb-0.5" > How did you hear about us?</ Label >
313+ < input
314+ type = "hidden"
315+ name = "referralSource"
316+ value = { selectedReferralSource ?? "" }
317+ />
318+ < RadioGroup
319+ value = { selectedReferralSource }
320+ onValueChange = { setSelectedReferralSource }
321+ className = "flex flex-wrap gap-2"
322+ >
323+ { shuffledReferralSources . map ( ( option ) => (
324+ < RadioGroupItem
325+ key = { option }
326+ id = { `referral-${ option } ` }
327+ label = { option }
328+ value = { option }
329+ variant = "button/small"
330+ />
331+ ) ) }
332+ </ RadioGroup >
333+ { selectedReferralSource === "Other" && (
334+ < div className = "mt-2" >
335+ < Input
336+ name = "referralSourceOther"
337+ type = "text"
338+ placeholder = "What was the source?"
339+ spellCheck = { false }
340+ />
341+ </ div >
342+ ) }
343+ </ InputGroup >
344+
345+ < InputGroup className = "mt-1" >
346+ < Label > What role fits you best?</ Label >
347+ < input type = "hidden" name = "role" value = { selectedRole } />
348+ < Select < string , string >
349+ value = { selectedRole }
350+ setValue = { setSelectedRole }
351+ placeholder = "Select an option"
352+ variant = "secondary/small"
353+ dropdownIcon
354+ items = { shuffledRoles }
355+ className = "h-8 rounded border-charcoal-800 bg-charcoal-750 px-3 text-sm hover:border-charcoal-600 hover:bg-charcoal-650"
356+ >
357+ { ( items ) =>
358+ items . map ( ( item ) => (
359+ < SelectItem key = { item } value = { item } >
360+ < span className = "text-text-bright" > { item } </ span >
361+ </ SelectItem >
362+ ) )
363+ }
364+ </ Select >
365+ { selectedRole === "Other" && (
366+ < div className = "mt-2" >
367+ < Input
368+ name = "roleOther"
369+ type = "text"
370+ placeholder = "What's your role?"
371+ spellCheck = { false }
372+ />
373+ </ div >
374+ ) }
375+ </ InputGroup >
376+ </ >
248377 ) }
249378
250379 < FormButtons
0 commit comments