"use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import Link from "next/link"; import { toast } from "@app/hooks/useToast"; import { useCallback, useEffect, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@app/components/ui/card"; import CopyTextBox from "@app/components/CopyTextBox"; 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 CreateSiteForm from "../[orgId]/settings/sites/CreateSiteForm"; type Step = "org" | "site" | "resources"; const orgSchema = z.object({ orgName: z.string().min(1, { message: "Organization name is required" }), orgId: z.string().min(1, { message: "Organization ID is required" }) }); export default function StepperForm() { const [currentStep, setCurrentStep] = useState("org"); const [orgIdTaken, setOrgIdTaken] = useState(false); const [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); const [error, setError] = useState(null); const orgForm = useForm>({ resolver: zodResolver(orgSchema), defaultValues: { orgName: "", orgId: "" } }); const api = createApiClient(useEnvContext()); const router = useRouter(); const checkOrgIdAvailability = useCallback(async (value: string) => { if (loading) { return; } try { const res = await api.get(`/org/checkId`, { params: { orgId: value } }); setOrgIdTaken(res.status !== 404); } catch (error) { setOrgIdTaken(false); } }, []); 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 }); if (res && res.status === 201) { // setCurrentStep("site"); router.push(`/${values.orgId}/settings/sites/create`); } } catch (e) { console.error(e); setError( formatAxiosError(e, "An error occurred while creating org") ); } setLoading(false); } return ( <> New Organization Create your organization, site, and resources
1
Create Org
2
Create Site
3
Create Resources
{currentStep === "org" && (
( Organization Name { // 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, "-")} /> This is the display name for your organization. )} /> ( Organization ID This is the unique identifier for your organization. This is separate from the display name. )} /> {orgIdTaken && ( Organization ID is already taken. Please choose a different one. )} {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); }; }