"use client"; import { useState, useEffect, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { AlertCircle, CheckCircle2, Building2, Zap, Check, ChevronsUpDown, ArrowUpDown } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { createApiClient, formatAxiosError } from "@/lib/api"; import { useEnvContext } from "@/hooks/useEnvContext"; import { toast } from "@/hooks/useToast"; import { ListDomainsResponse } from "@server/routers/domain/listDomains"; import { AxiosResponse } from "axios"; import { cn } from "@/lib/cn"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { sanitizeInputRaw, finalizeSubdomainSanitize, validateByDomainType, isValidSubdomainStructure } from "@/lib/subdomain-utils"; type OrganizationDomain = { domainId: string; baseDomain: string; verified: boolean; type: "ns" | "cname" | "wildcard"; }; type AvailableOption = { domainNamespaceId: string; fullDomain: string; domainId: string; }; type DomainOption = { id: string; domain: string; type: "organization" | "provided" | "provided-search"; verified?: boolean; domainType?: "ns" | "cname" | "wildcard"; domainId?: string; domainNamespaceId?: string; }; interface DomainPicker2Props { orgId: string; onDomainChange?: (domainInfo: { domainId: string; domainNamespaceId?: string; type: "organization" | "provided"; subdomain?: string; fullDomain: string; baseDomain: string; }) => void; cols?: number; } export default function DomainPicker2({ orgId, onDomainChange, cols = 2 }: DomainPicker2Props) { const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); const [subdomainInput, setSubdomainInput] = useState(""); const [selectedBaseDomain, setSelectedBaseDomain] = useState(null); const [availableOptions, setAvailableOptions] = useState( [] ); const [organizationDomains, setOrganizationDomains] = useState< OrganizationDomain[] >([]); const [loadingDomains, setLoadingDomains] = useState(false); const [open, setOpen] = useState(false); // Provided domain search states const [userInput, setUserInput] = useState(""); const [isChecking, setIsChecking] = useState(false); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); const [selectedProvidedDomain, setSelectedProvidedDomain] = useState(null); useEffect(() => { const loadOrganizationDomains = async () => { setLoadingDomains(true); try { const response = await api.get< AxiosResponse >(`/org/${orgId}/domains`); if (response.status === 200) { const domains = response.data.data.domains .filter( (domain) => domain.type === "ns" || domain.type === "cname" || domain.type === "wildcard" ) .map((domain) => ({ ...domain, type: domain.type as "ns" | "cname" | "wildcard" })); setOrganizationDomains(domains); // Auto-select first available domain if (domains.length > 0) { // Select the first organization domain const firstOrgDomain = domains[0]; const domainOption: DomainOption = { id: `org-${firstOrgDomain.domainId}`, domain: firstOrgDomain.baseDomain, type: "organization", verified: firstOrgDomain.verified, domainType: firstOrgDomain.type, domainId: firstOrgDomain.domainId }; setSelectedBaseDomain(domainOption); onDomainChange?.({ domainId: firstOrgDomain.domainId, type: "organization", subdomain: undefined, fullDomain: firstOrgDomain.baseDomain, baseDomain: firstOrgDomain.baseDomain }); } else if (build === "saas" || build === "enterprise") { // If no organization domains, select the provided domain option const domainOptionText = build === "enterprise" ? "Provided Domain" : "Free Provided Domain"; const freeDomainOption: DomainOption = { id: "provided-search", domain: domainOptionText, type: "provided-search" }; setSelectedBaseDomain(freeDomainOption); } } } catch (error) { console.error("Failed to load organization domains:", error); toast({ variant: "destructive", title: "Error", description: "Failed to load organization domains" }); } finally { setLoadingDomains(false); } }; loadOrganizationDomains(); }, [orgId, api]); const checkAvailability = useCallback( async (input: string) => { if (!input.trim()) { setAvailableOptions([]); setIsChecking(false); return; } setIsChecking(true); try { const checkSubdomain = input .toLowerCase() .replace(/\./g, "-") .replace(/[^a-z0-9-]/g, "") .replace(/-+/g, "-"); } catch (error) { console.error("Failed to check domain availability:", error); setAvailableOptions([]); toast({ variant: "destructive", title: "Error", description: "Failed to check domain availability" }); } finally { setIsChecking(false); } }, [api] ); const debouncedCheckAvailability = useCallback( debounce(checkAvailability, 500), [checkAvailability] ); useEffect(() => { if (selectedBaseDomain?.type === "provided-search") { setProvidedDomainsShown(3); setSelectedProvidedDomain(null); if (userInput.trim()) { setIsChecking(true); debouncedCheckAvailability(userInput); } else { setAvailableOptions([]); setIsChecking(false); } } }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); const generateDropdownOptions = (): DomainOption[] => { const options: DomainOption[] = []; organizationDomains.forEach((orgDomain) => { options.push({ id: `org-${orgDomain.domainId}`, domain: orgDomain.baseDomain, type: "organization", verified: orgDomain.verified, domainType: orgDomain.type, domainId: orgDomain.domainId }); }); if (build === "saas" || build === "enterprise") { const domainOptionText = build === "enterprise" ? "Provided Domain" : "Free Provided Domain"; options.push({ id: "provided-search", domain: domainOptionText, type: "provided-search" }); } return options; }; const dropdownOptions = generateDropdownOptions(); const finalizeSubdomain = (sub: string, base: DomainOption): string => { const sanitized = finalizeSubdomainSanitize(sub); if (!sanitized) { toast({ variant: "destructive", title: "Invalid subdomain", description: `The input "${sub}" was removed because it's not valid.`, }); return ""; } const ok = validateByDomainType(sanitized, { type: base.type === "provided-search" ? "provided-search" : "organization", domainType: base.domainType }); if (!ok) { toast({ variant: "destructive", title: "Invalid subdomain", description: `"${sub}" could not be made valid for ${base.domain}.`, }); return ""; } if (sub !== sanitized) { toast({ title: "Subdomain sanitized", description: `"${sub}" was corrected to "${sanitized}"`, }); } return sanitized; }; const handleSubdomainChange = (value: string) => { const raw = sanitizeInputRaw(value); setSubdomainInput(raw); setSelectedProvidedDomain(null); if (selectedBaseDomain?.type === "organization") { const fullDomain = raw ? `${raw}.${selectedBaseDomain.domain}` : selectedBaseDomain.domain; onDomainChange?.({ domainId: selectedBaseDomain.domainId!, type: "organization", subdomain: raw || undefined, fullDomain, baseDomain: selectedBaseDomain.domain }); } }; const handleProvidedDomainInputChange = (value: string) => { setUserInput(value); if (selectedProvidedDomain) { setSelectedProvidedDomain(null); onDomainChange?.({ domainId: "", type: "provided", subdomain: undefined, fullDomain: "", baseDomain: "" }); } }; const handleBaseDomainSelect = (option: DomainOption) => { let sub = subdomainInput; sub = finalizeSubdomain(sub, option); setSubdomainInput(sub); if (option.type === "provided-search") { setUserInput(""); setAvailableOptions([]); setSelectedProvidedDomain(null); } setSelectedBaseDomain(option); setOpen(false); if (option.domainType === "cname") { sub = ""; setSubdomainInput(""); } const fullDomain = sub ? `${sub}.${option.domain}` : option.domain; onDomainChange?.({ domainId: option.domainId || "", domainNamespaceId: option.domainNamespaceId, type: option.type === "provided-search" ? "provided" : "organization", subdomain: sub || undefined, fullDomain, baseDomain: option.domain }); }; const handleProvidedDomainSelect = (option: AvailableOption) => { setSelectedProvidedDomain(option); const parts = option.fullDomain.split("."); const subdomain = parts[0]; const baseDomain = parts.slice(1).join("."); onDomainChange?.({ domainId: option.domainId, domainNamespaceId: option.domainNamespaceId, type: "provided", subdomain, fullDomain: option.fullDomain, baseDomain }); }; const isSubdomainValid = selectedBaseDomain && subdomainInput ? validateByDomainType(subdomainInput, { type: selectedBaseDomain.type === "provided-search" ? "provided-search" : "organization", domainType: selectedBaseDomain.domainType }) : true; const showSubdomainInput = selectedBaseDomain && selectedBaseDomain.type === "organization" && selectedBaseDomain.domainType !== "cname"; const showProvidedDomainSearch = selectedBaseDomain?.type === "provided-search"; const sortedAvailableOptions = [...availableOptions].sort((a, b) => { const comparison = a.fullDomain.localeCompare(b.fullDomain); return sortOrder === "asc" ? comparison : -comparison; }); const displayedProvidedOptions = sortedAvailableOptions.slice( 0, providedDomainsShown ); const hasMoreProvided = sortedAvailableOptions.length > providedDomainsShown; return (
{ if (showProvidedDomainSearch) { handleProvidedDomainInputChange(e.target.value); } else { handleSubdomainChange(e.target.value); } }} /> {showSubdomainInput && subdomainInput && !isValidSubdomainStructure(subdomainInput) && (

This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.

)} {showSubdomainInput && !subdomainInput && (

{t("domainPickerEnterSubdomainOrLeaveBlank")}

)} {showProvidedDomainSearch && !userInput && (

{t("domainPickerEnterSubdomainToSearch")}

)}
{t("domainPickerNoDomainsFound")}
{organizationDomains.length > 0 && ( <> {organizationDomains.map( (orgDomain) => ( handleBaseDomainSelect( { id: `org-${orgDomain.domainId}`, domain: orgDomain.baseDomain, type: "organization", verified: orgDomain.verified, domainType: orgDomain.type, domainId: orgDomain.domainId } ) } className="mx-2 rounded-md" disabled={ !orgDomain.verified } >
{ orgDomain.baseDomain } {orgDomain.type.toUpperCase()}{" "} •{" "} {orgDomain.verified ? "Verified" : "Unverified"}
) )}
{(build === "saas" || build === "enterprise") && ( )} )} {(build === "saas" || build === "enterprise") && ( handleBaseDomainSelect({ id: "provided-search", domain: build === "enterprise" ? "Provided Domain" : "Free Provided Domain", type: "provided-search" }) } className="mx-2 rounded-md" >
{build === "enterprise" ? "Provided Domain" : "Free Provided Domain"} {t( "domainPickerSearchForAvailableDomains" )}
)}
{showProvidedDomainSearch && (
{isChecking && (
{t("domainPickerCheckingAvailability")}
)} {!isChecking && sortedAvailableOptions.length === 0 && userInput.trim() && ( {t("domainPickerNoMatchingDomains")} )} {!isChecking && sortedAvailableOptions.length > 0 && (
{ const option = displayedProvidedOptions.find( (opt) => opt.domainNamespaceId === value ); if (option) { handleProvidedDomainSelect(option); } }} className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} > {displayedProvidedOptions.map((option) => ( ))} {hasMoreProvided && ( )}
)}
)} {loadingDomains && (
{t("domainPickerLoadingDomains")}
)}
); } 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); }; }