import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { BuildingOffice2Icon, GlobeAltIcon } from "@heroicons/react/20/solid"; import { RadioGroup } from "@radix-ui/react-radio-group"; import { json, redirect, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { BackgroundWrapper } from "~/components/BackgroundWrapper"; import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } 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 { Hint } from "~/components/primitives/Hint"; 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 { useFaviconUrl } from "~/hooks/useFaviconUrl"; import { useFeatures } from "~/hooks/useFeatures"; import { createOrganization } from "~/models/organization.server"; import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server"; import { requireUser, requireUserId } from "~/services/session.server"; import { extractDomain, faviconUrl } from "~/utils/favicon"; import { organizationPath, rootPath } from "~/utils/pathBuilder"; const schema = z.object({ orgName: z.string().min(3).max(50), companySize: z.string().optional(), companyUrl: z.string().optional(), }); export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const presenter = new NewOrganizationPresenter(); const { hasOrganizations } = await presenter.call({ userId: userId }); return typedjson({ hasOrganizations, }); }; export const action: ActionFunction = async ({ request }) => { const user = await requireUser(request); const formData = await request.formData(); const submission = parseWithZod(formData, { schema }); if (submission.status !== "success") { return json(submission.reply()); } try { const companySize = submission.value.companySize ?? null; const onboardingData: Record = {}; if (submission.value.companyUrl) { onboardingData.companyUrl = submission.value.companyUrl; } if (submission.value.companySize) { onboardingData.companySize = submission.value.companySize; } let avatar: { type: "image"; url: string } | undefined; if (submission.value.companyUrl) { const domain = extractDomain(submission.value.companyUrl); if (domain) { avatar = { type: "image", url: faviconUrl(domain) }; } } const organization = await createOrganization({ title: submission.value.orgName, userId: user.id, companySize, onboardingData: Object.keys(onboardingData).length > 0 ? onboardingData : undefined, avatar, }); // A promo code carried over from the /promo landing page (via cookie) is // redeemed later, once the org is activated through plan selection and its // usage entitlement exists — not here, where there's nothing to grant onto. const url = new URL(request.url); const code = url.searchParams.get("code"); const configurationId = url.searchParams.get("configurationId"); const integration = url.searchParams.get("integration"); const next = url.searchParams.get("next"); if (code && configurationId && integration === "vercel") { const params = new URLSearchParams({ code, configurationId, integration, }); if (next) { params.set("next", next); } return redirect(`${organizationPath(organization)}/projects/new?${params.toString()}`); } return redirect(organizationPath(organization)); } catch (error: any) { return json({ errors: { body: error.message } }, { status: 400 }); } }; export default function NewOrganizationPage() { const { hasOrganizations } = useTypedLoaderData(); const lastSubmission = useActionData(); const { isManagedCloud } = useFeatures(); const navigation = useNavigation(); const [companyUrl, setCompanyUrl] = useState(""); const faviconUrl = useFaviconUrl(companyUrl); const [faviconError, setFaviconError] = useState(false); const [form, { orgName }] = useForm({ id: "create-organization", lastResult: lastSubmission as any, onValidate({ formData }) { return parseWithZod(formData, { schema }); }, shouldRevalidate: "onSubmit", shouldValidate: "onSubmit", }); const isLoading = navigation.state === "submitting" || navigation.state === "loading"; const urlIcon = faviconUrl && !faviconError ? ( setFaviconError(true)} onLoad={() => setFaviconError(false)} /> ) : ( GlobeAltIcon ); return ( } title="Create an Organization" />
Normally your company name. {orgName.errors} {isManagedCloud && ( <> { setCompanyUrl(e.target.value); setFaviconError(false); }} /> Add your company URL and we'll use it as your organization's logo. )} Create } cancelButton={ hasOrganizations ? ( Cancel ) : null } />
); }