import { zodResolver } from "@hookform/resolvers/zod"; import { t } from "@lingui/core/macro"; import { env } from "next-runtime-env"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { HiCheck, HiXMark } from "react-icons/hi2"; import { twMerge } from "tailwind-merge"; import { z } from "zod"; import Button from "~/components/Button"; import Input from "~/components/Input"; import { useDebounce } from "~/hooks/useDebounce"; import { useModal } from "~/providers/modal"; import { usePopup } from "~/providers/popup"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; import LoadingSpinner from "./LoadingSpinner"; const schema = z.object({ name: z .string() .min(1, { message: t`Workspace name is required` }) .max(64, { message: t`Workspace name cannot exceed 64 characters` }), slug: z .string() .min(3, { message: t`URL must be at least 3 characters long`, }) .max(64, { message: t`URL cannot exceed 64 characters` }) .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, { message: t`URL can only contain letters, numbers, and hyphens`, }) .optional() .or(z.literal("")), }); type FormValues = z.infer; export function NewWorkspaceForm() { const { closeModal } = useModal(); const { showPopup } = usePopup(); const { switchWorkspace, availableWorkspaces } = useWorkspace(); const { register, handleSubmit, formState: { errors }, watch, trigger, clearErrors, } = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", slug: "", }, mode: "onSubmit", }); const utils = api.useUtils(); const hasAvailableWorkspaces = availableWorkspaces.length > 0; const slug = watch("slug"); const [debouncedSlug] = useDebounce(slug, 500); const isTyping = slug !== debouncedSlug; // Validate slug only after debounce useEffect(() => { if (isTyping) { // Clear errors while typing clearErrors("slug"); } else if (debouncedSlug) { // Validate after debounce void trigger("slug"); } }, [isTyping, debouncedSlug, trigger, clearErrors]); const checkWorkspaceSlugAvailability = api.workspace.checkSlugAvailability.useQuery( { workspaceSlug: debouncedSlug ?? "", }, { enabled: !!debouncedSlug && debouncedSlug.length >= 3 && !errors.slug, }, ); const isWorkspaceSlugAvailable = checkWorkspaceSlugAvailability.data; const createWorkspace = api.workspace.create.useMutation({ onSuccess: (values) => { if (values.publicId && values.name) { void utils.workspace.all.invalidate(); switchWorkspace({ publicId: values.publicId, name: values.name, description: values.description, slug: values.slug, plan: values.plan, cardPrefix: values.cardPrefix, role: "admin", weekStartDay: 1, }); closeModal(); } }, onError: () => { showPopup({ header: t`Unable to create workspace`, message: t`Please try again later, or contact customer support.`, icon: "error", }); }, }); useEffect(() => { const nameElement: HTMLElement | null = document.querySelector("#workspace-name"); if (nameElement) nameElement.focus(); }, []); const isValidSlug = slug && slug.length >= 3 && !errors.slug; const onSubmit = (values: FormValues) => { // Don't submit if slug is provided but not available if (values.slug && isWorkspaceSlugAvailable?.isAvailable === false) { return; } createWorkspace.mutate({ name: values.name, slug: values.slug, }); }; const isSlugAvailable = isValidSlug && isWorkspaceSlugAvailable?.isAvailable && !isWorkspaceSlugAvailable?.isReserved; return (

{t`New workspace`}

{ if (e.key === "Enter") { e.preventDefault(); await handleSubmit(onSubmit)(); } }} />
= 3 && !errors.slug ? ( isWorkspaceSlugAvailable?.isAvailable ? ( ) : checkWorkspaceSlugAvailability.isPending || isTyping ? ( ) : null ) : null } onKeyDown={async (e) => { if (e.key === "Enter") { e.preventDefault(); await handleSubmit(onSubmit)(); } }} />
); }