import { zodResolver } from '@hookform/resolvers/zod'; import { zOnboardingProject } from '@openpanel/validation'; import { useMutation, useQuery } from '@tanstack/react-query'; import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'; import { BuildingIcon, MonitorIcon, ServerIcon, SmartphoneIcon, } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Controller, type SubmitHandler, useForm, useWatch, } from 'react-hook-form'; import { z } from 'zod'; import AnimateHeight from '@/components/animate-height'; import { ButtonContainer } from '@/components/button-container'; import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label'; import TagInput from '@/components/forms/tag-input'; import FullPageLoadingState from '@/components/full-page-loading-state'; import { Button } from '@/components/ui/button'; import { Combobox } from '@/components/ui/combobox'; import { Label } from '@/components/ui/label'; import { useClientSecret } from '@/hooks/use-client-secret'; import { handleError, useTRPC } from '@/integrations/trpc/react'; import { cn } from '@/utils/cn'; const validateSearch = z.object({ inviteId: z.string().optional(), }); export const Route = createFileRoute('/_steps/onboarding/project')({ component: Component, validateSearch, beforeLoad: ({ context }) => { if (!context.session?.session) { throw redirect({ to: '/onboarding' }); } }, loader: async ({ context, location }) => { const search = validateSearch.safeParse(location.search); if (search.success && search.data.inviteId) { await context.queryClient.prefetchQuery( context.trpc.organization.getInvite.queryOptions({ inviteId: search.data.inviteId, }) ); } }, pendingComponent: FullPageLoadingState, }); type IForm = z.infer; function Component() { const trpc = useTRPC(); const { data: organizations } = useQuery( trpc.organization.list.queryOptions(undefined, { initialData: [] }) ); const [, setSecret] = useClientSecret(); const navigate = useNavigate(); const mutation = useMutation( trpc.onboarding.project.mutationOptions({ onError: handleError, onSuccess(res) { setSecret(res.secret); navigate({ to: '/onboarding/$projectId/connect', params: { projectId: res.projectId!, }, }); }, }) ); const form = useForm({ resolver: zodResolver(zOnboardingProject), defaultValues: { organization: '', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, project: '', domain: '', cors: [], website: false, app: false, backend: false, }, }); const isWebsite = useWatch({ name: 'website', control: form.control, }); const isApp = useWatch({ name: 'app', control: form.control, }); const isBackend = useWatch({ name: 'backend', control: form.control, }); const domain = useWatch({ name: 'domain', control: form.control, }); const [showCorsInput, setShowCorsInput] = useState(false); useEffect(() => { if (!isWebsite) { form.setValue('domain', null); form.setValue('cors', []); setShowCorsInput(false); } }, [isWebsite, form]); const onSubmit: SubmitHandler = (values) => { mutation.mutate(values); }; useEffect(() => { form.clearErrors(); }, [isWebsite, isApp, isBackend]); return (
{organizations.length > 0 ? ( { return (
item.id) .map((item) => ({ label: item.name, value: item.id, })) ?? [] } onChange={field.onChange} placeholder="Select workspace" value={field.value} />
); }} /> ) : ( <> ( ({ value: item, label: item, }))} onChange={field.onChange} placeholder="Select timezone" searchable value={field.value} /> )} /> )}
{[ { key: 'website' as const, label: 'Website', Icon: MonitorIcon, active: isWebsite, }, { key: 'app' as const, label: 'App', Icon: SmartphoneIcon, active: isApp, }, { key: 'backend' as const, label: 'Backend / API', Icon: ServerIcon, active: isBackend, }, ].map(({ key, label, Icon, active }) => ( ))}
{(form.formState.errors.website?.message || form.formState.errors.app?.message || form.formState.errors.backend?.message) && (

At least one type must be selected

)}
{ const raw = e.target.value.trim(); if (!raw) { return; } const hasProtocol = raw.startsWith('http://') || raw.startsWith('https://'); const value = hasProtocol ? raw : `https://${raw}`; form.setValue('domain', value, { shouldValidate: true }); if (form.getValues().cors.length === 0) { form.setValue('cors', [value]); } }} /> {domain && ( <>
( { field.onChange( newValue.map((item: string) => { const trimmed = item.trim(); if ( trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed === '*' ) { return trimmed; } return `https://${trimmed}`; }) ); }} placeholder="Accept events from these domains" renderTag={(tag: string) => tag === '*' ? 'Accept events from any domains' : tag } value={field.value ?? []} /> )} />
)}
); }