Skip to content

Commit d1ea8d8

Browse files
authored
Fix(webapp) onboarding fixes (#3189)
### Fixes and improvements to the onboarding questions: **This change is worth double checking @matt-aitken** - Update to the Button.tsx file: it now takes `isLoading` that shows a spinner in the middle of the button (replacing the button text and any icons) and sets it to `disabled`. It does this nicely by keeping the button width the same so there's no layout shift. **Other fixes** - Fixes an issue where if you type a custom option in the "What technologies do you use" question, it doesn't check the list to see if it matches. Now it checks the box if you've typed an option from that list. - When we randomize the list of onboarding question options, we now store the position they appeared in the list
1 parent e64b101 commit d1ea8d8

File tree

5 files changed

+168
-67
lines changed

5 files changed

+168
-67
lines changed

apps/webapp/app/components/onboarding/TechnologyPicker.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,22 @@ export function TechnologyPicker({
210210

211211
const addCustomValue = useCallback(() => {
212212
const trimmed = otherInputValue.trim();
213-
if (trimmed && !customValues.includes(trimmed) && !value.includes(trimmed)) {
213+
if (!trimmed) return;
214+
215+
const matchedOption = TECHNOLOGY_OPTIONS.find(
216+
(opt) => opt.toLowerCase() === trimmed.toLowerCase()
217+
);
218+
219+
if (matchedOption) {
220+
if (!value.includes(matchedOption)) {
221+
onChange([...value, matchedOption]);
222+
}
223+
} else if (!customValues.includes(trimmed) && !value.includes(trimmed)) {
214224
onCustomValuesChange([...customValues, trimmed]);
215-
setOtherInputValue("");
216225
}
217-
}, [otherInputValue, customValues, onCustomValuesChange, value]);
226+
227+
setOtherInputValue("");
228+
}, [otherInputValue, customValues, onCustomValuesChange, value, onChange]);
218229

219230
const handleOtherKeyDown = useCallback(
220231
(e: React.KeyboardEvent) => {

apps/webapp/app/components/primitives/Buttons.tsx

Lines changed: 82 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { Link, type LinkProps, NavLink, type NavLinkProps } from "@remix-run/react";
2-
import React, { forwardRef, type ReactNode, useImperativeHandle, useRef } from "react";
2+
import React, {
3+
forwardRef,
4+
type ReactNode,
5+
useEffect,
6+
useImperativeHandle,
7+
useRef,
8+
useState,
9+
} from "react";
310
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
411
import { cn } from "~/utils/cn";
512
import { ShortcutKey } from "./ShortcutKey";
613
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
714
import { Icon, type RenderIcon } from "./Icon";
15+
import { Spinner } from "./Spinner";
816

917
const sizes = {
1018
small: {
@@ -180,6 +188,7 @@ export type ButtonContentPropsType = {
180188
tooltip?: ReactNode;
181189
iconSpacing?: string;
182190
hideShortcutKey?: boolean;
191+
isLoading?: boolean;
183192
};
184193

185194
export function ButtonContent(props: ButtonContentPropsType) {
@@ -196,7 +205,19 @@ export function ButtonContent(props: ButtonContentPropsType) {
196205
tooltip,
197206
iconSpacing,
198207
hideShortcutKey,
208+
isLoading,
199209
} = props;
210+
211+
const [showSpinner, setShowSpinner] = useState(false);
212+
useEffect(() => {
213+
if (!isLoading) {
214+
setShowSpinner(false);
215+
return;
216+
}
217+
const timer = setTimeout(() => setShowSpinner(true), 200);
218+
return () => clearTimeout(timer);
219+
}, [isLoading]);
220+
200221
const variation = allVariants.variant[props.variant];
201222

202223
const btnClassName = cn(allVariants.$all, variation.button);
@@ -217,56 +238,64 @@ export function ButtonContent(props: ButtonContentPropsType) {
217238

218239
const buttonContent = (
219240
<div className={cn("flex", fullWidth ? "" : "w-fit text-xxs", btnClassName, className)}>
220-
<div
221-
className={cn(
222-
textAlignLeft ? "text-left" : "justify-center",
223-
"flex w-full items-center",
224-
iconSpacingClassName,
225-
iconSpacing
226-
)}
227-
>
228-
{LeadingIcon && (
229-
<Icon
230-
icon={LeadingIcon}
231-
className={cn(
232-
iconClassName,
233-
variation.icon,
234-
leadingIconClassName,
235-
"shrink-0 justify-start"
236-
)}
237-
/>
238-
)}
241+
<div className={cn("relative", "flex w-full items-center")}>
242+
<div
243+
className={cn(
244+
textAlignLeft ? "text-left" : "justify-center",
245+
"flex w-full items-center",
246+
iconSpacingClassName,
247+
iconSpacing,
248+
showSpinner && "invisible"
249+
)}
250+
>
251+
{LeadingIcon && (
252+
<Icon
253+
icon={LeadingIcon}
254+
className={cn(
255+
iconClassName,
256+
variation.icon,
257+
leadingIconClassName,
258+
"shrink-0 justify-start"
259+
)}
260+
/>
261+
)}
239262

240-
{text &&
241-
(typeof text === "string" ? (
242-
<span className={cn("mx-auto grow self-center truncate", textColorClassName)}>
243-
{text}
244-
</span>
245-
) : (
246-
<>{text}</>
247-
))}
248-
249-
{shortcut &&
250-
!tooltip &&
251-
props.shortcutPosition === "before-trailing-icon" &&
252-
renderShortcutKey()}
253-
254-
{TrailingIcon && (
255-
<Icon
256-
icon={TrailingIcon}
257-
className={cn(
258-
iconClassName,
259-
variation.icon,
260-
trailingIconClassName,
261-
"shrink-0 justify-end"
262-
)}
263-
/>
264-
)}
263+
{text &&
264+
(typeof text === "string" ? (
265+
<span className={cn("mx-auto grow self-center truncate", textColorClassName)}>
266+
{text}
267+
</span>
268+
) : (
269+
<>{text}</>
270+
))}
271+
272+
{shortcut &&
273+
!tooltip &&
274+
props.shortcutPosition === "before-trailing-icon" &&
275+
renderShortcutKey()}
265276

266-
{shortcut &&
267-
!tooltip &&
268-
(!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") &&
269-
renderShortcutKey()}
277+
{TrailingIcon && (
278+
<Icon
279+
icon={TrailingIcon}
280+
className={cn(
281+
iconClassName,
282+
variation.icon,
283+
trailingIconClassName,
284+
"shrink-0 justify-end"
285+
)}
286+
/>
287+
)}
288+
289+
{shortcut &&
290+
!tooltip &&
291+
(!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") &&
292+
renderShortcutKey()}
293+
</div>
294+
{showSpinner && (
295+
<span className="absolute inset-0 flex items-center justify-center">
296+
<Spinner className="size-3.5" color="white" />
297+
</span>
298+
)}
270299
</div>
271300
</div>
272301
);
@@ -298,6 +327,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
298327
const innerRef = useRef<HTMLButtonElement>(null);
299328
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
300329

330+
const isDisabled = disabled || props.isLoading;
331+
301332
useShortcutKeys({
302333
shortcut: props.shortcut,
303334
action: (e) => {
@@ -307,14 +338,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
307338
e.stopPropagation();
308339
}
309340
},
310-
disabled: disabled || !props.shortcut,
341+
disabled: isDisabled || !props.shortcut,
311342
});
312343

313344
return (
314345
<button
315346
className={cn("group/button outline-none focus-custom", props.fullWidth ? "w-full" : "")}
316347
type={type}
317-
disabled={disabled}
348+
disabled={isDisabled}
318349
onClick={onClick}
319350
name={props.name}
320351
value={props.value}

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

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { Input } from "~/components/primitives/Input";
2222
import { InputGroup } from "~/components/primitives/InputGroup";
2323
import { Label } from "~/components/primitives/Label";
2424
import { Select, SelectItem } from "~/components/primitives/Select";
25-
import { ButtonSpinner } from "~/components/primitives/Spinner";
25+
2626
import { prisma } from "~/db.server";
2727
import { featuresForRequest } from "~/features.server";
2828
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
@@ -169,6 +169,8 @@ const schema = z.object({
169169
technologiesOther: z.string().optional(),
170170
goals: z.string().optional(),
171171
goalsOther: z.string().optional(),
172+
workingOnPositions: z.string().optional(),
173+
goalsPositions: z.string().optional(),
172174
});
173175

174176
export const action: ActionFunction = async ({ request, params }) => {
@@ -200,10 +202,25 @@ export const action: ActionFunction = async ({ request, params }) => {
200202
}
201203
}
202204

205+
const numberArraySchema = z.array(z.number());
206+
function safeParseNumberArray(value: string | undefined): number[] | undefined {
207+
if (!value) return undefined;
208+
try {
209+
const result = numberArraySchema.safeParse(JSON.parse(value));
210+
return result.success && result.data.length > 0 ? result.data : undefined;
211+
} catch {
212+
return undefined;
213+
}
214+
}
215+
203216
const onboardingData: Record<string, Prisma.InputJsonValue> = {};
204217

205218
const workingOn = safeParseStringArray(submission.value.workingOn);
206-
if (workingOn) onboardingData.workingOn = workingOn;
219+
if (workingOn) {
220+
onboardingData.workingOn = workingOn;
221+
const workingOnPositions = safeParseNumberArray(submission.value.workingOnPositions);
222+
if (workingOnPositions) onboardingData.workingOnPositions = workingOnPositions;
223+
}
207224

208225
if (submission.value.workingOnOther) {
209226
onboardingData.workingOnOther = submission.value.workingOnOther;
@@ -216,7 +233,11 @@ export const action: ActionFunction = async ({ request, params }) => {
216233
if (technologiesOther) onboardingData.technologiesOther = technologiesOther;
217234

218235
const goals = safeParseStringArray(submission.value.goals);
219-
if (goals) onboardingData.goals = goals;
236+
if (goals) {
237+
onboardingData.goals = goals;
238+
const goalsPositions = safeParseNumberArray(submission.value.goalsPositions);
239+
if (goalsPositions) onboardingData.goalsPositions = goalsPositions;
240+
}
220241

221242
if (submission.value.goalsOther) {
222243
onboardingData.goalsOther = submission.value.goalsOther;
@@ -376,6 +397,13 @@ export default function Page() {
376397
<InputGroup>
377398
<Label>What are you working on?</Label>
378399
<input type="hidden" name="workingOn" value={JSON.stringify(selectedWorkingOn)} />
400+
<input
401+
type="hidden"
402+
name="workingOnPositions"
403+
value={JSON.stringify(
404+
selectedWorkingOn.map((v) => shuffledWorkingOn.indexOf(v) + 1)
405+
)}
406+
/>
379407
<MultiSelectField
380408
value={selectedWorkingOn}
381409
setValue={setSelectedWorkingOn}
@@ -421,6 +449,13 @@ export default function Page() {
421449
<InputGroup>
422450
<Label>What are you trying to do with Trigger.dev?</Label>
423451
<input type="hidden" name="goals" value={JSON.stringify(selectedGoals)} />
452+
<input
453+
type="hidden"
454+
name="goalsPositions"
455+
value={JSON.stringify(
456+
selectedGoals.map((v) => shuffledGoals.indexOf(v) + 1)
457+
)}
458+
/>
424459
<MultiSelectField
425460
value={selectedGoals}
426461
setValue={setSelectedGoals}
@@ -445,13 +480,8 @@ export default function Page() {
445480

446481
<FormButtons
447482
confirmButton={
448-
<Button
449-
type="submit"
450-
variant={"primary/small"}
451-
disabled={isLoading}
452-
TrailingIcon={isLoading ? ButtonSpinner : undefined}
453-
>
454-
{isLoading ? "Creating…" : "Create"}
483+
<Button type="submit" variant={"primary/small"} isLoading={isLoading}>
484+
Create
455485
</Button>
456486
}
457487
cancelButton={

apps/webapp/app/routes/_app.orgs.new/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export default function NewOrganizationPage() {
220220

221221
<FormButtons
222222
confirmButton={
223-
<Button type="submit" variant={"primary/small"} disabled={isLoading}>
223+
<Button type="submit" variant={"primary/small"} isLoading={isLoading}>
224224
Create
225225
</Button>
226226
}

0 commit comments

Comments
 (0)