"use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "@app/hooks/useToast"; import { useCallback, useEffect, useState } from "react"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useUserContext } from "@app/hooks/useUserContext"; import { build } from "@server/build"; import { Separator } from "@/components/ui/separator"; import { z } from "zod"; import { useRouter, useSearchParams } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { useTranslations } from "next-intl"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@app/components/ui/collapsible"; import { ArrowRight, ChevronsUpDown } from "lucide-react"; import { cn } from "@app/lib/cn"; type Step = "org" | "site" | "resources"; export default function StepperForm() { const [currentStep, setCurrentStep] = useState("org"); const [orgIdTaken, setOrgIdTaken] = useState(false); const t = useTranslations(); const { env } = useEnvContext(); const { user } = useUserContext(); const [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); // Removed error state, now using toast for API errors const [orgCreated, setOrgCreated] = useState(false); const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const orgSchema = z.object({ orgName: z.string().min(1, { message: t("orgNameRequired") }), orgId: z.string().min(1, { message: t("orgIdRequired") }), subnet: z.string().min(1, { message: t("subnetRequired") }), utilitySubnet: z.string().min(1, { message: t("subnetRequired") }) }); const orgForm = useForm({ resolver: zodResolver(orgSchema), defaultValues: { orgName: "", orgId: "", subnet: "", utilitySubnet: "" } }); const api = createApiClient(useEnvContext()); const router = useRouter(); const searchParams = useSearchParams(); const isFirstOrg = searchParams.get("firstOrg") != null; // Fetch default subnet on component mount useEffect(() => { fetchDefaultSubnet(); }, []); // Prefill org name and id when build is saas and firstOrg query param is set useEffect(() => { if (build !== "saas" || !user || !isFirstOrg) return; const orgName = user.email ? `${user.email}'s Organization` : "My Organization"; const orgId = `org_${user.userId}`; orgForm.setValue("orgName", orgName); orgForm.setValue("orgId", orgId); debouncedCheckOrgIdAvailability(orgId); }, []); const fetchDefaultSubnet = async () => { try { const res = await api.get(`/pick-org-defaults`); if (res && res.data && res.data.data) { orgForm.setValue("subnet", res.data.data.subnet); orgForm.setValue("utilitySubnet", res.data.data.utilitySubnet); } } catch (e) { console.error("Failed to fetch default subnet:", e); toast({ title: t("error"), description: t("setupFailedToFetchSubnet"), variant: "destructive" }); } }; const checkOrgIdAvailability = useCallback( async (value: string) => { if (loading || orgCreated) { return; } try { const res = await api.get(`/org/checkId`, { params: { orgId: value } }); setOrgIdTaken(res.status !== 404); } catch (error) { setOrgIdTaken(false); } }, [loading, orgCreated, api] ); const debouncedCheckOrgIdAvailability = useCallback( debounce(checkOrgIdAvailability, 300), [checkOrgIdAvailability] ); const generateId = (name: string) => { // Replace any character that is not a letter, number, space, or hyphen with a hyphen // Also collapse multiple hyphens and trim return name .toLowerCase() .replace(/[^a-z0-9\s-]/g, "-") .replace(/\s+/g, "-") .replace(/-+/g, "-") .replace(/^-+|-+$/g, ""); }; async function orgSubmit(values: z.infer) { if (orgIdTaken) { return; } setLoading(true); try { const res = await api.put(`/org`, { orgId: values.orgId, name: values.orgName, subnet: values.subnet, utilitySubnet: values.utilitySubnet }); if (res && res.status === 201) { setOrgCreated(true); router.push(`/${values.orgId}/settings/sites/create`); } } catch (e) { console.error(e); toast({ title: t("error"), description: formatAxiosError(e, t("orgErrorCreate")), variant: "destructive" }); } setLoading(false); } return (

{t("setupNewOrg")}

{t("setupCreate")}

1
{t("setupCreateOrg")}
2
{t("siteCreate")}
3
{t("setupCreateResources")}
{currentStep === "org" && (
( {t("setupOrgName")} { // Prevent "/" in orgName input const sanitizedValue = e.target.value.replace( /\//g, "-" ); const orgId = generateId(sanitizedValue); orgForm.setValue( "orgId", orgId ); orgForm.setValue( "orgName", sanitizedValue ); debouncedCheckOrgIdAvailability( orgId ); }} value={field.value.replace( /\//g, "-" )} /> {t("orgDisplayName")} )} /> ( {t("orgId")} {t("setupIdentifierMessage")} )} />
( {t("setupSubnetAdvanced")} {t("setupSubnetDescription")} )} /> ( {t("setupUtilitySubnet")} {t( "setupUtilitySubnetDescription" )} )} />
{orgIdTaken && !orgCreated ? ( {t("setupErrorIdentifier")} ) : null} {/* Error Alert removed, errors now shown as toast */}
)}
); } function debounce any>( func: T, wait: number ): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; return (...args: Parameters) => { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { func(...args); }, wait); }; }