"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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@app/components/ui/card"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Separator } from "@/components/ui/separator"; import { z } from "zod"; import { useRouter } 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"; 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 [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); const [error, setError] = useState(null); const [orgCreated, setOrgCreated] = 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") }) }); const orgForm = useForm>({ resolver: zodResolver(orgSchema), defaultValues: { orgName: "", orgId: "", subnet: "" } }); const api = createApiClient(useEnvContext()); const router = useRouter(); // Fetch default subnet on component mount useEffect(() => { fetchDefaultSubnet(); }, []); 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); } } catch (e) { console.error("Failed to fetch default subnet:", e); toast({ title: "Error", description: "Failed to fetch default subnet", 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 }); if (res && res.status === 201) { setOrgCreated(true); router.push(`/${values.orgId}/settings/sites/create`); } } catch (e) { console.error(e); setError(formatAxiosError(e, t("orgErrorCreate"))); } 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" )} )} /> {env.flags.enableClients && ( ( Subnet Network subnet for this organization. A default value has been provided. )} /> )} {orgIdTaken && !orgCreated ? ( {t("setupErrorIdentifier")} ) : null} {error && ( {error} )}
)}
); } 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); }; }