Skip to content

Commit 0dc7a38

Browse files
committed
Adds new questions to the New project route
1 parent 37f7423 commit 0dc7a38

File tree

1 file changed

+184
-9
lines changed
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new

1 file changed

+184
-9
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { conform, useForm } from "@conform-to/react";
22
import { parse } from "@conform-to/zod";
33
import { 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";
65
import { Form, useActionData, useNavigation } from "@remix-run/react";
76
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
7+
import type { Prisma } from "@trigger.dev/database";
88
import invariant from "tiny-invariant";
9+
import { useEffect, useState } from "react";
910
import { z } from "zod";
1011
import { BackgroundWrapper } from "~/components/BackgroundWrapper";
1112
import { Feedback } from "~/components/Feedback";
@@ -19,7 +20,9 @@ import { FormTitle } from "~/components/primitives/FormTitle";
1920
import { Input } from "~/components/primitives/Input";
2021
import { InputGroup } from "~/components/primitives/InputGroup";
2122
import { Label } from "~/components/primitives/Label";
23+
import { Select, SelectItem } from "~/components/primitives/Select";
2224
import { ButtonSpinner } from "~/components/primitives/Spinner";
25+
import { TechnologyPicker } from "~/components/onboarding/TechnologyPicker";
2326
import { prisma } from "~/db.server";
2427
import { featuresForRequest } from "~/features.server";
2528
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
@@ -34,6 +37,33 @@ import {
3437
} from "~/utils/pathBuilder";
3538
import { 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+
3767
export 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) {
90118
const 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

95128
export 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

Comments
 (0)