Skip to content

Commit 38a9ee3

Browse files
committed
Update the User create modal with new questions
1 parent 1eb189d commit 38a9ee3

1 file changed

Lines changed: 159 additions & 30 deletions

File tree

apps/webapp/app/routes/confirm-basic-details.tsx

Lines changed: 159 additions & 30 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";
3-
import { ArrowRightIcon, EnvelopeIcon, HeartIcon, UserIcon } from "@heroicons/react/20/solid";
3+
import { ArrowRightIcon, EnvelopeIcon, UserIcon } from "@heroicons/react/20/solid";
44
import { 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";
67
import { Form, useActionData } from "@remix-run/react";
78
import { motion } from "framer-motion";
8-
import { forwardRef, useState } from "react";
9+
import { forwardRef, useEffect, useState } from "react";
910
import { z } from "zod";
1011
import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout";
1112
import { BackgroundWrapper } from "~/components/BackgroundWrapper";
@@ -18,6 +19,8 @@ import { Hint } from "~/components/primitives/Hint";
1819
import { Input } from "~/components/primitives/Input";
1920
import { InputGroup } from "~/components/primitives/InputGroup";
2021
import { Label } from "~/components/primitives/Label";
22+
import { RadioGroupItem } from "~/components/primitives/RadioButton";
23+
import { Select, SelectItem } from "~/components/primitives/Select";
2124
import { prisma } from "~/db.server";
2225
import { useFeatures } from "~/hooks/useFeatures";
2326
import { useUser } from "~/hooks/useUser";
@@ -27,6 +30,40 @@ import { requireUserId } from "~/services/session.server";
2730
import { rootPath } from "~/utils/pathBuilder";
2831
import { 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+
3067
function 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

Comments
 (0)