import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { conformZodMessage, parseWithZod } from "@conform-to/zod"; import { ArrowRightIcon, EnvelopeIcon, UserIcon } from "@heroicons/react/20/solid"; 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, useNavigation } from "@remix-run/react"; import { motion } from "framer-motion"; import { forwardRef, useEffect, useState } from "react"; import { z } from "zod"; import { UserGroupIcon } from "~/assets/icons/UserGroupIcon"; import { BackgroundWrapper } from "~/components/BackgroundWrapper"; import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout"; import { Button } from "~/components/primitives/Buttons"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormButtons } from "~/components/primitives/FormButtons"; import { FormError } from "~/components/primitives/FormError"; import { FormTitle } from "~/components/primitives/FormTitle"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { RadioGroupItem } from "~/components/primitives/RadioButton"; import { Select, SelectItem } from "~/components/primitives/Select"; import { prisma } from "~/db.server"; import { useFeatures } from "~/hooks/useFeatures"; import { useUser } from "~/hooks/useUser"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { updateUser } from "~/models/user.server"; import { requireUserId } from "~/services/session.server"; import { rootPath } from "~/utils/pathBuilder"; import { getVercelInstallParams } from "~/v3/vercel"; const referralSourceOptions = [ "Search engine", "YouTube", "Twitter/X", "LinkedIn", "Word of mouth", "AI assistant/LLM", "Blog/article", "Event", "Other", ] as const; const roleOptions = [ "Founder", "Staff/principal engineer", "Senior software engineer", "Software engineer", "AI/ML engineer", "Engineering manager", "Product engineer", "Non technical builder using AI tools", "Student/learner", "Other", ] as const; function shuffleArray(arr: T[]): T[] { const shuffled = [...arr]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } function createSchema( constraints: { isEmailUnique?: (email: string) => Promise; } = {} ) { return z .object({ name: z.string().min(3, "Your name must be at least 3 characters").max(50), email: z .string() .email() .superRefine((email, ctx) => { if (constraints.isEmailUnique === undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: conformZodMessage.VALIDATION_UNDEFINED, }); } else { return constraints.isEmailUnique(email).then((isUnique) => { if (isUnique) { return; } ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Email is already being used by a different account", }); }); } }), confirmEmail: z.string(), referralSource: z.string().optional(), 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", path: ["confirmEmail"], }); } export const action: ActionFunction = async ({ request }) => { const userId = await requireUserId(request); const formData = await request.formData(); const formSchema = createSchema({ isEmailUnique: async (email) => { const existingUser = await prisma.user.findFirst({ where: { email, }, }); if (!existingUser) { return true; } if (existingUser.id === userId) { return true; } return false; }, }); const submission = await parseWithZod(formData, { schema: formSchema, async: true }); if (submission.status !== "success") { return json(submission.reply()); } try { const onboardingData: Record = {}; 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; } } 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; } } const referralSourceForLegacy = submission.value.referralSource === "Other" && submission.value.referralSourceOther ? `Other: ${submission.value.referralSourceOther}` : submission.value.referralSource; await updateUser({ id: userId, name: submission.value.name, email: submission.value.email, referralSource: referralSourceForLegacy, onboardingData: Object.keys(onboardingData).length > 0 ? onboardingData : undefined, }); const vercelParams = getVercelInstallParams(request); let redirectUrl = rootPath(); if (vercelParams) { const params = new URLSearchParams({ code: vercelParams.code, configurationId: vercelParams.configurationId, integration: "vercel", }); if (vercelParams.next) { params.set("next", vercelParams.next); } redirectUrl = `/orgs/new?${params.toString()}`; } return redirectWithSuccessMessage(redirectUrl, request, "Your details have been updated."); } catch (error: any) { return json({ errors: { body: error.message } }, { status: 400 }); } }; const HandIcon = forwardRef(({}, ref) => { return (
); }); const MotionHand = motion(HandIcon); export default function Page() { const user = useUser(); 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(""); const [shuffledReferralSources, setShuffledReferralSources] = useState([ ...referralSourceOptions, ]); const [shuffledRoles, setShuffledRoles] = useState([...roleOptions]); useEffect(() => { const nonOtherReferral = referralSourceOptions.filter((r) => r !== "Other"); setShuffledReferralSources([...shuffleArray(nonOtherReferral), "Other"]); const nonOtherRoles = roleOptions.filter((r) => r !== "Other"); setShuffledRoles([...shuffleArray(nonOtherRoles), "Other"]); }, []); const [form, { name, email, confirmEmail }] = useForm({ id: "confirm-basic-details", lastResult: lastSubmission as any, onValidate({ formData }) { return parseWithZod(formData, { schema: createSchema() }); }, shouldRevalidate: "onSubmit", }); const shouldShowConfirm = user.email !== enteredEmail || user.email === ""; return (
} description="We just need you to confirm a couple of details." />
{name.errors} { setEnteredEmail(e.target.value); }} placeholder="Your email address" icon={EnvelopeIcon} spellCheck={false} /> {email.errors} {shouldShowConfirm ? ( {confirmEmail.errors} ) : ( <> )} {isManagedCloud && ( <>
{shuffledReferralSources.map((option) => ( ))} {selectedReferralSource === "Other" && (
)}
value={selectedRole} setValue={setSelectedRole} placeholder="Select an option" aria-labelledby="role-label" variant="secondary/small" dropdownIcon icon={} items={shuffledRoles} className="h-8 min-w-0 border-0 bg-background-hover pl-2 text-sm text-text-dimmed ring-border-bright transition hover:bg-secondary hover:text-text-dimmed hover:ring-1" text={(v) => (v ? {v} : undefined)} > {(items) => items.map((item) => ( {item} )) } {selectedRole === "Other" && (
)}
)} Continue } />
); }