import { zodResolver } from "@hookform/resolvers/zod"; import { t } from "@lingui/core/macro"; import { env } from "next-runtime-env"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { HiBolt, HiCheck, HiCheckBadge, HiInformationCircle, 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 Toggle from "~/components/Toggle"; 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` }), slug: z .string() .min(3, { message: t`URL must be at least 3 characters long`, }) .max(24, { message: t`URL cannot exceed 24 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, setValue, } = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", slug: "", }, mode: "onSubmit", }); const utils = api.useUtils(); const hasAvailableWorkspaces = availableWorkspaces.length > 0; const isCloudEnv = env("NEXT_PUBLIC_KAN_ENV") === "cloud"; const slug = watch("slug"); const [debouncedSlug] = useDebounce(slug, 500); const isTyping = slug !== debouncedSlug; // Pro toggle state management const [isProToggleEnabled, setIsProToggleEnabled] = useState(false); const [lastAvailableSlug, setLastAvailableSlug] = useState(""); // 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: async (values, variables) => { 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, role: "admin", }); // If in cloud and Pro toggle is enabled, create checkout session for pro if (env("NEXT_PUBLIC_KAN_ENV") === "cloud" && isProToggleEnabled) { try { const response = await fetch( "/api/stripe/create_checkout_session", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ slug: slug || undefined, workspacePublicId: values.publicId, cancelUrl: "/settings/workspace?upgrade=pro", successUrl: "/boards", }), }, ); const data = await response.json(); const url = (data as { url: string }).url; if (url) { window.location.href = url; return; // Don't close modal if redirecting to checkout } } catch (error) { console.error("Error creating checkout session:", error); showPopup({ header: t`Error upgrading to Pro`, message: t`Workspace created successfully. You can upgrade later in settings.`, icon: "warning", }); } } 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 [shouldShowBenefits, setShouldShowBenefits] = useState(false); const isValidSlug = slug && slug.length >= 3 && !errors.slug; useEffect(() => { if ( isCloudEnv && !checkWorkspaceSlugAvailability.isPending && isValidSlug ) { const isAvailable = isWorkspaceSlugAvailable?.isAvailable === true; setShouldShowBenefits(isAvailable); // Automatically enable Pro toggle when slug is available if (isAvailable) { setIsProToggleEnabled(true); setLastAvailableSlug(slug); } } }, [ isValidSlug, isWorkspaceSlugAvailable?.isAvailable, checkWorkspaceSlugAvailability.isPending, slug, isCloudEnv, ]); // Reset benefits when slug becomes invalid useEffect(() => { if (!isValidSlug) { setShouldShowBenefits(false); } }, [isValidSlug]); // Handle Pro toggle changes const handleProToggleChange = () => { if (isProToggleEnabled) { // Turning off: clear the slug and disable Pro setValue("slug", ""); setIsProToggleEnabled(false); } else { // Turning on: restore the last available slug if we have one if (lastAvailableSlug) { setValue("slug", lastAvailableSlug); } setIsProToggleEnabled(true); } }; const showProBenefits = isProToggleEnabled; 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: !isCloudEnv && values.slug ? values.slug : undefined, }); }; 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)(); } }} /> {slug && slug.length >= 3 && shouldShowBenefits && (

{t`Custom URLs require upgrading to a Pro plan`}

)}
{showProBenefits && (
{t`Unlimited members`} {t`Launch offer`}
{t`Custom workspace URL`}
{t`Board analytics`} {t`Coming soon`}
)}
{/* Pro Toggle - only show in cloud environment */} {isCloudEnv && ( )}
); }