11import { conform , useForm } from "@conform-to/react" ;
22import { parse } from "@conform-to/zod" ;
33import { FolderIcon } from "@heroicons/react/20/solid" ;
4- import type { ActionFunction , LoaderFunctionArgs } from "@remix-run/node" ;
5- import { json } from "@remix-run/node" ;
4+ import { json , type ActionFunction , type LoaderFunctionArgs } from "@remix-run/node" ;
65import { Form , useActionData , useNavigation } from "@remix-run/react" ;
76import { redirect , typedjson , useTypedLoaderData } from "remix-typedjson" ;
7+ import type { Prisma } from "@trigger.dev/database" ;
88import invariant from "tiny-invariant" ;
9+ import { useEffect , useState } from "react" ;
910import { z } from "zod" ;
1011import { BackgroundWrapper } from "~/components/BackgroundWrapper" ;
1112import { Feedback } from "~/components/Feedback" ;
@@ -19,7 +20,9 @@ import { FormTitle } from "~/components/primitives/FormTitle";
1920import { Input } from "~/components/primitives/Input" ;
2021import { InputGroup } from "~/components/primitives/InputGroup" ;
2122import { Label } from "~/components/primitives/Label" ;
23+ import { Select , SelectItem } from "~/components/primitives/Select" ;
2224import { ButtonSpinner } from "~/components/primitives/Spinner" ;
25+ import { TechnologyPicker } from "~/components/onboarding/TechnologyPicker" ;
2326import { prisma } from "~/db.server" ;
2427import { featuresForRequest } from "~/features.server" ;
2528import { redirectWithErrorMessage , redirectWithSuccessMessage } from "~/models/message.server" ;
@@ -34,6 +37,33 @@ import {
3437} from "~/utils/pathBuilder" ;
3538import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server" ;
3639
40+ const workingOnOptions = [
41+ "AI agent" ,
42+ "Media processing pipeline" ,
43+ "Media generation with AI" ,
44+ "Event-driven workflow" ,
45+ "Realtime streaming" ,
46+ "Internal tool or background job" ,
47+ "Other/not sure yet" ,
48+ ] as const ;
49+
50+ const goalOptions = [
51+ "Ship a production workflow" ,
52+ "Prototype or explore" ,
53+ "Migrate an existing system" ,
54+ "Learn how Trigger works" ,
55+ "Evaluate against alternatives" ,
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+
3767export async function loader ( { params, request } : LoaderFunctionArgs ) {
3868 const userId = await requireUserId ( request ) ;
3969 const { organizationSlug } = OrganizationParamsSchema . parse ( params ) ;
@@ -62,14 +92,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
6292 throw new Response ( null , { status : 404 , statusText : "Organization not found" } ) ;
6393 }
6494
65- //if you don't have v3 access, you must select a plan
6695 const { isManagedCloud } = featuresForRequest ( request ) ;
6796 if ( isManagedCloud && ! organization . v3Enabled ) {
6897 return redirect ( selectPlanPath ( { slug : organizationSlug } ) ) ;
6998 }
7099
71100 const url = new URL ( request . url ) ;
72-
73101 const message = url . searchParams . get ( "message" ) ;
74102
75103 return typedjson ( {
@@ -90,6 +118,11 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
90118const schema = z . object ( {
91119 projectName : z . string ( ) . min ( 3 , "Project name must have at least 3 characters" ) . max ( 50 ) ,
92120 projectVersion : z . enum ( [ "v2" , "v3" ] ) ,
121+ workingOn : z . string ( ) . optional ( ) ,
122+ workingOnOther : z . string ( ) . optional ( ) ,
123+ technologies : z . string ( ) . optional ( ) ,
124+ technologiesOther : z . string ( ) . optional ( ) ,
125+ goals : z . string ( ) . optional ( ) ,
93126} ) ;
94127
95128export const action : ActionFunction = async ( { request, params } ) => {
@@ -104,21 +137,50 @@ export const action: ActionFunction = async ({ request, params }) => {
104137 return json ( submission ) ;
105138 }
106139
107- // Check for Vercel integration params in URL
108140 const url = new URL ( request . url ) ;
109141 const code = url . searchParams . get ( "code" ) ;
110142 const configurationId = url . searchParams . get ( "configurationId" ) ;
111143 const next = url . searchParams . get ( "next" ) ;
112144
145+ const onboardingData : Record < string , Prisma . InputJsonValue > = { } ;
146+
147+ if ( submission . value . workingOn ) {
148+ const workingOn = JSON . parse ( submission . value . workingOn ) as string [ ] ;
149+ if ( workingOn . length > 0 ) {
150+ onboardingData . workingOn = workingOn ;
151+ }
152+ }
153+ if ( submission . value . workingOnOther ) {
154+ onboardingData . workingOnOther = submission . value . workingOnOther ;
155+ }
156+ if ( submission . value . technologies ) {
157+ const technologies = JSON . parse ( submission . value . technologies ) as string [ ] ;
158+ if ( technologies . length > 0 ) {
159+ onboardingData . technologies = technologies ;
160+ }
161+ }
162+ if ( submission . value . technologiesOther ) {
163+ const technologiesOther = JSON . parse ( submission . value . technologiesOther ) as string [ ] ;
164+ if ( technologiesOther . length > 0 ) {
165+ onboardingData . technologiesOther = technologiesOther ;
166+ }
167+ }
168+ if ( submission . value . goals ) {
169+ const goals = JSON . parse ( submission . value . goals ) as string [ ] ;
170+ if ( goals . length > 0 ) {
171+ onboardingData . goals = goals ;
172+ }
173+ }
174+
113175 try {
114176 const project = await createProject ( {
115177 organizationSlug : organizationSlug ,
116178 name : submission . value . projectName ,
117179 userId,
118180 version : submission . value . projectVersion ,
181+ onboardingData : Object . keys ( onboardingData ) . length > 0 ? onboardingData : undefined ,
119182 } ) ;
120183
121- // If this is a Vercel integration flow, generate state and redirect to connect
122184 if ( code && configurationId ) {
123185 const environment = await prisma . runtimeEnvironment . findFirst ( {
124186 where : {
@@ -195,7 +257,6 @@ export default function Page() {
195257
196258 const [ form , { projectName, projectVersion } ] = useForm ( {
197259 id : "create-project" ,
198- // TODO: type this
199260 lastSubmission : lastSubmission as any ,
200261 onValidate ( { formData } ) {
201262 return parse ( formData , { schema } ) ;
@@ -205,10 +266,25 @@ export default function Page() {
205266 const navigation = useNavigation ( ) ;
206267 const isLoading = navigation . state === "submitting" || navigation . state === "loading" ;
207268
269+ const [ selectedWorkingOn , setSelectedWorkingOn ] = useState < string [ ] > ( [ ] ) ;
270+ const [ workingOnOther , setWorkingOnOther ] = useState ( "" ) ;
271+ const [ selectedTechnologies , setSelectedTechnologies ] = useState < string [ ] > ( [ ] ) ;
272+ const [ customTechnologies , setCustomTechnologies ] = useState < string [ ] > ( [ ] ) ;
273+ const [ selectedGoals , setSelectedGoals ] = useState < string [ ] > ( [ ] ) ;
274+
275+ const [ shuffledWorkingOn , setShuffledWorkingOn ] = useState < string [ ] > ( [ ...workingOnOptions ] ) ;
276+
277+ useEffect ( ( ) => {
278+ const nonOther = workingOnOptions . filter ( ( o ) => o !== "Other/not sure yet" ) ;
279+ setShuffledWorkingOn ( [ ...shuffleArray ( nonOther ) , "Other/not sure yet" ] ) ;
280+ } , [ ] ) ;
281+
282+ const showWorkingOnOther = selectedWorkingOn . includes ( "Other/not sure yet" ) ;
283+
208284 return (
209285 < AppContainer className = "bg-charcoal-900" >
210286 < BackgroundWrapper >
211- < MainCenteredContainer className = "max-w-[26rem ] rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg" >
287+ < MainCenteredContainer className = "max-w-[29rem ] rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg" >
212288 < div >
213289 < FormTitle
214290 LeadingIcon = { < FolderIcon className = "size-7 text-indigo-500" /> }
@@ -223,7 +299,9 @@ export default function Page() {
223299 ) }
224300 < Fieldset >
225301 < InputGroup >
226- < Label htmlFor = { projectName . id } > Project name</ Label >
302+ < Label htmlFor = { projectName . id } >
303+ Project name < span className = "text-text-bright" > *</ span >
304+ </ Label >
227305 < Input
228306 { ...conform . input ( projectName , { type : "text" } ) }
229307 placeholder = "Your project name"
@@ -237,6 +315,103 @@ export default function Page() {
237315 ) : (
238316 < input { ...conform . input ( projectVersion , { type : "hidden" } ) } value = { "v2" } />
239317 ) }
318+
319+ < div className = "border-t border-charcoal-700" />
320+ < InputGroup >
321+ < Label > What are you working on?</ Label >
322+ < input
323+ type = "hidden"
324+ name = "workingOn"
325+ value = { JSON . stringify ( selectedWorkingOn ) }
326+ />
327+ < Select < string [ ] , string >
328+ value = { selectedWorkingOn }
329+ setValue = { setSelectedWorkingOn }
330+ placeholder = "Select options"
331+ variant = "secondary/small"
332+ dropdownIcon
333+ items = { shuffledWorkingOn }
334+ className = "h-8"
335+ text = { ( value ) =>
336+ value . length === 0
337+ ? undefined
338+ : value . length <= 2
339+ ? value . join ( ", " )
340+ : `${ value . slice ( 0 , 2 ) . join ( ", " ) } +${ value . length - 2 } more`
341+ }
342+ >
343+ { ( items ) =>
344+ items . map ( ( item ) => (
345+ < SelectItem key = { item } value = { item } checkPosition = "left" >
346+ < span className = "text-text-bright" > { item } </ span >
347+ </ SelectItem >
348+ ) )
349+ }
350+ </ Select >
351+ { showWorkingOnOther && (
352+ < >
353+ < input type = "hidden" name = "workingOnOther" value = { workingOnOther } />
354+ < Input
355+ type = "text"
356+ value = { workingOnOther }
357+ onChange = { ( e ) => setWorkingOnOther ( e . target . value ) }
358+ placeholder = "Tell us what you're working on"
359+ spellCheck = { false }
360+ className = "mt-2"
361+ />
362+ </ >
363+ ) }
364+ </ InputGroup >
365+
366+ < InputGroup >
367+ < Label > What technologies are you using?</ Label >
368+ < input
369+ type = "hidden"
370+ name = "technologies"
371+ value = { JSON . stringify ( selectedTechnologies ) }
372+ />
373+ < input
374+ type = "hidden"
375+ name = "technologiesOther"
376+ value = { JSON . stringify ( customTechnologies ) }
377+ />
378+ < TechnologyPicker
379+ value = { selectedTechnologies }
380+ onChange = { setSelectedTechnologies }
381+ customValues = { customTechnologies }
382+ onCustomValuesChange = { setCustomTechnologies }
383+ />
384+ </ InputGroup >
385+
386+ < InputGroup >
387+ < Label > What are you trying to do with Trigger.dev?</ Label >
388+ < input type = "hidden" name = "goals" value = { JSON . stringify ( selectedGoals ) } />
389+ < Select < string [ ] , string >
390+ value = { selectedGoals }
391+ setValue = { setSelectedGoals }
392+ placeholder = "Select options"
393+ variant = "secondary/small"
394+ dropdownIcon
395+ items = { [ ...goalOptions ] }
396+ className = "h-8"
397+ text = { ( value ) =>
398+ value . length === 0
399+ ? undefined
400+ : value . length <= 2
401+ ? value . join ( ", " )
402+ : `${ value . slice ( 0 , 2 ) . join ( ", " ) } +${ value . length - 2 } more`
403+ }
404+ >
405+ { ( items ) =>
406+ items . map ( ( item ) => (
407+ < SelectItem key = { item } value = { item } checkPosition = "left" >
408+ < span className = "text-text-bright" > { item } </ span >
409+ </ SelectItem >
410+ ) )
411+ }
412+ </ Select >
413+ </ InputGroup >
414+
240415 < FormButtons
241416 confirmButton = {
242417 < Button
0 commit comments