diff --git a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx index 3d822e4b69..9c9f9a6b24 100644 --- a/apps/webapp/app/components/onboarding/TechnologyPicker.tsx +++ b/apps/webapp/app/components/onboarding/TechnologyPicker.tsx @@ -210,11 +210,22 @@ export function TechnologyPicker({ const addCustomValue = useCallback(() => { const trimmed = otherInputValue.trim(); - if (trimmed && !customValues.includes(trimmed) && !value.includes(trimmed)) { + if (!trimmed) return; + + const matchedOption = TECHNOLOGY_OPTIONS.find( + (opt) => opt.toLowerCase() === trimmed.toLowerCase() + ); + + if (matchedOption) { + if (!value.includes(matchedOption)) { + onChange([...value, matchedOption]); + } + } else if (!customValues.includes(trimmed) && !value.includes(trimmed)) { onCustomValuesChange([...customValues, trimmed]); - setOtherInputValue(""); } - }, [otherInputValue, customValues, onCustomValuesChange, value]); + + setOtherInputValue(""); + }, [otherInputValue, customValues, onCustomValuesChange, value, onChange]); const handleOtherKeyDown = useCallback( (e: React.KeyboardEvent) => { diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 5c52a5d95d..8ba196b5dc 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -1,10 +1,18 @@ import { Link, type LinkProps, NavLink, type NavLinkProps } from "@remix-run/react"; -import React, { forwardRef, type ReactNode, useImperativeHandle, useRef } from "react"; +import React, { + forwardRef, + type ReactNode, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; import { cn } from "~/utils/cn"; import { ShortcutKey } from "./ShortcutKey"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip"; import { Icon, type RenderIcon } from "./Icon"; +import { Spinner } from "./Spinner"; const sizes = { small: { @@ -180,6 +188,7 @@ export type ButtonContentPropsType = { tooltip?: ReactNode; iconSpacing?: string; hideShortcutKey?: boolean; + isLoading?: boolean; }; export function ButtonContent(props: ButtonContentPropsType) { @@ -196,7 +205,19 @@ export function ButtonContent(props: ButtonContentPropsType) { tooltip, iconSpacing, hideShortcutKey, + isLoading, } = props; + + const [showSpinner, setShowSpinner] = useState(false); + useEffect(() => { + if (!isLoading) { + setShowSpinner(false); + return; + } + const timer = setTimeout(() => setShowSpinner(true), 200); + return () => clearTimeout(timer); + }, [isLoading]); + const variation = allVariants.variant[props.variant]; const btnClassName = cn(allVariants.$all, variation.button); @@ -217,56 +238,64 @@ export function ButtonContent(props: ButtonContentPropsType) { const buttonContent = (
-
- {LeadingIcon && ( - - )} +
+
+ {LeadingIcon && ( + + )} - {text && - (typeof text === "string" ? ( - - {text} - - ) : ( - <>{text} - ))} - - {shortcut && - !tooltip && - props.shortcutPosition === "before-trailing-icon" && - renderShortcutKey()} - - {TrailingIcon && ( - - )} + {text && + (typeof text === "string" ? ( + + {text} + + ) : ( + <>{text} + ))} + + {shortcut && + !tooltip && + props.shortcutPosition === "before-trailing-icon" && + renderShortcutKey()} - {shortcut && - !tooltip && - (!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") && - renderShortcutKey()} + {TrailingIcon && ( + + )} + + {shortcut && + !tooltip && + (!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") && + renderShortcutKey()} +
+ {showSpinner && ( + + + + )}
); @@ -298,6 +327,8 @@ export const Button = forwardRef( const innerRef = useRef(null); useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement); + const isDisabled = disabled || props.isLoading; + useShortcutKeys({ shortcut: props.shortcut, action: (e) => { @@ -307,14 +338,14 @@ export const Button = forwardRef( e.stopPropagation(); } }, - disabled: disabled || !props.shortcut, + disabled: isDisabled || !props.shortcut, }); return ( } cancelButton={ diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index b1d8fd83af..fb072518c5 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -220,7 +220,7 @@ export default function NewOrganizationPage() { + } diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index fad485e6d1..ed86818655 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -4,7 +4,7 @@ import { ArrowRightIcon, EnvelopeIcon, UserGroupIcon, UserIcon } from "@heroicon import { HandRaisedIcon } from "@heroicons/react/24/solid"; import { RadioGroup } from "@radix-ui/react-radio-group"; import { json, type ActionFunction } from "@remix-run/node"; -import { Form, useActionData } from "@remix-run/react"; +import { Form, useActionData, useNavigation } from "@remix-run/react"; import { motion } from "framer-motion"; import { forwardRef, useEffect, useState } from "react"; import { z } from "zod"; @@ -99,6 +99,8 @@ function createSchema( referralSourceOther: z.string().optional(), role: z.string().optional(), roleOther: z.string().optional(), + referralSourcePosition: z.coerce.number().optional(), + rolePosition: z.coerce.number().optional(), }) .refine((value) => value.email === value.confirmEmail, { message: "Emails must match", @@ -141,6 +143,9 @@ export const action: ActionFunction = async ({ request }) => { if (submission.value.referralSource) { onboardingData.referralSource = submission.value.referralSource; + if (submission.value.referralSourcePosition) { + onboardingData.referralSourcePosition = String(submission.value.referralSourcePosition); + } if (submission.value.referralSource === "Other" && submission.value.referralSourceOther) { onboardingData.referralSourceOther = submission.value.referralSourceOther; } @@ -148,6 +153,9 @@ export const action: ActionFunction = async ({ request }) => { if (submission.value.role) { onboardingData.role = submission.value.role; + if (submission.value.rolePosition) { + onboardingData.rolePosition = String(submission.value.rolePosition); + } if (submission.value.role === "Other" && submission.value.roleOther) { onboardingData.roleOther = submission.value.roleOther; } @@ -201,6 +209,8 @@ export default function Page() { const lastSubmission = useActionData(); const [enteredEmail, setEnteredEmail] = useState(user.email ?? ""); const { isManagedCloud } = useFeatures(); + const navigation = useNavigation(); + const isSubmitting = navigation.state === "submitting" || navigation.state === "loading"; const [selectedReferralSource, setSelectedReferralSource] = useState(); const [selectedRole, setSelectedRole] = useState(""); @@ -317,6 +327,15 @@ export default function Page() { name="referralSource" value={selectedReferralSource ?? ""} /> + + value={selectedRole} setValue={setSelectedRole} @@ -384,7 +408,12 @@ export default function Page() { + }