"use client"; import { useState, useEffect, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { AlertCircle, CheckCircle2, Building2, Zap, 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTranslations } from "next-intl"; type OrganizationDomain = { domainId: string; baseDomain: string; verified: boolean; type: "ns" | "cname"; }; type AvailableOption = { domainNamespaceId: string; fullDomain: string; domainId: string; }; type DomainOption = { id: string; domain: string; type: "organization" | "provided"; verified?: boolean; domainType?: "ns" | "cname"; domainId?: string; domainNamespaceId?: string; subdomain?: string; }; interface DomainPickerProps { orgId: string; onDomainChange?: (domainInfo: { domainId: string; domainNamespaceId?: string; type: "organization" | "provided"; subdomain?: string; fullDomain: string; baseDomain: string; }) => void; } export default function DomainPicker({ orgId, onDomainChange }: DomainPickerProps) { const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); const [userInput, setUserInput] = useState(""); const [selectedOption, setSelectedOption] = useState( null ); const [availableOptions, setAvailableOptions] = useState( [] ); const [isChecking, setIsChecking] = useState(false); const [organizationDomains, setOrganizationDomains] = useState< OrganizationDomain[] >([]); const [loadingDomains, setLoadingDomains] = useState(false); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [activeTab, setActiveTab] = useState< "all" | "organization" | "provided" >("all"); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); 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" ) .map((domain) => ({ ...domain, type: domain.type as "ns" | "cname" })); setOrganizationDomains(domains); } } 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]); // Generate domain options based on user input const generateDomainOptions = (): DomainOption[] => { const options: DomainOption[] = []; if (!userInput.trim()) return options; // Check if input is more than one level deep (contains multiple dots) const isMultiLevel = (userInput.match(/\./g) || []).length > 1; // Add organization domain options organizationDomains.forEach((orgDomain) => { if (orgDomain.type === "cname") { // For CNAME domains, check if the user input matches exactly if ( orgDomain.baseDomain.toLowerCase() === userInput.toLowerCase() ) { options.push({ id: `org-${orgDomain.domainId}`, domain: orgDomain.baseDomain, type: "organization", verified: orgDomain.verified, domainType: "cname", domainId: orgDomain.domainId }); } } else if (orgDomain.type === "ns") { // For NS domains, check if the user input could be a subdomain const userInputLower = userInput.toLowerCase(); const baseDomainLower = orgDomain.baseDomain.toLowerCase(); // Check if user input ends with the base domain if (userInputLower.endsWith(`.${baseDomainLower}`)) { const subdomain = userInputLower.slice( 0, -(baseDomainLower.length + 1) ); options.push({ id: `org-${orgDomain.domainId}`, domain: userInput, type: "organization", verified: orgDomain.verified, domainType: "ns", domainId: orgDomain.domainId, subdomain: subdomain }); } else if (userInputLower === baseDomainLower) { // Exact match for base domain options.push({ id: `org-${orgDomain.domainId}`, domain: orgDomain.baseDomain, type: "organization", verified: orgDomain.verified, domainType: "ns", domainId: orgDomain.domainId }); } } }); // Add provided domain options (always try to match provided domains) availableOptions.forEach((option) => { options.push({ id: `provided-${option.domainNamespaceId}`, domain: option.fullDomain, type: "provided", domainNamespaceId: option.domainNamespaceId, domainId: option.domainId, }); }); // Sort options return options.sort((a, b) => { const comparison = a.domain.localeCompare(b.domain); return sortOrder === "asc" ? comparison : -comparison; }); }; const domainOptions = generateDomainOptions(); // Filter options based on active tab const filteredOptions = domainOptions.filter((option) => { if (activeTab === "all") return true; return option.type === activeTab; }); // Separate organization and provided options for pagination const organizationOptions = filteredOptions.filter( (opt) => opt.type === "organization" ); const allProvidedOptions = filteredOptions.filter( (opt) => opt.type === "provided" ); const providedOptions = allProvidedOptions.slice(0, providedDomainsShown); const hasMoreProvided = allProvidedOptions.length > providedDomainsShown; // Handle option selection const handleOptionSelect = (option: DomainOption) => { setSelectedOption(option); if (option.type === "organization") { if (option.domainType === "cname") { onDomainChange?.({ domainId: option.domainId!, type: "organization", subdomain: undefined, fullDomain: option.domain, baseDomain: option.domain }); } else if (option.domainType === "ns") { const subdomain = option.subdomain || ""; onDomainChange?.({ domainId: option.domainId!, type: "organization", subdomain: subdomain || undefined, fullDomain: option.domain, baseDomain: option.domain }); } } else if (option.type === "provided") { // Extract subdomain from full domain const parts = option.domain.split("."); const subdomain = parts[0]; const baseDomain = parts.slice(1).join("."); onDomainChange?.({ domainId: option.domainId!, domainNamespaceId: option.domainNamespaceId, type: "provided", subdomain: subdomain, fullDomain: option.domain, baseDomain: baseDomain }); } }; return (
{/* Domain Input */}
{ // Only allow letters, numbers, hyphens, and periods const validInput = e.target.value.replace( /[^a-zA-Z0-9.-]/g, "" ); setUserInput(validInput); }} />

{t("domainPickerDescription")}

{/* Tabs and Sort Toggle */}
setActiveTab( value as "all" | "organization" | "provided" ) } > {t("domainPickerTabAll")} {t("domainPickerTabOrganization")} {t("domainPickerTabProvided")}
{/* Loading State */} {isChecking && (
{t("domainPickerCheckingAvailability")}
)} {/* No Options */} {!isChecking && filteredOptions.length === 0 && userInput.trim() && ( {t("domainPickerNoMatchingDomains", { userInput })} )} {/* Domain Options */} {!isChecking && filteredOptions.length > 0 && (
{/* Organization Domains */} {organizationOptions.length > 0 && (

{t("domainPickerOrganizationDomains")}

{organizationOptions.map((option) => (
option.verified && handleOptionSelect(option) } >

{option.domain}

{/* */} {/* {option.domainType} */} {/* */} {option.verified ? ( ) : ( )}
{option.subdomain && (

{t( "domainPickerSubdomain", { subdomain: option.subdomain } )}

)} {!option.verified && (

Domain is unverified

)}
{selectedOption?.id === option.id && ( )}
))}
)} {/* Provided Domains */} {providedOptions.length > 0 && (
{t("domainPickerProvidedDomains")}
{providedOptions.map((option) => (
handleOptionSelect(option) } >

{option.domain}

{t( "domainPickerNamespace", { namespace: option.domainNamespaceId as string } )}

{selectedOption?.id === option.id && ( )}
))}
{hasMoreProvided && ( )}
)}
)}
); } 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); }; }