'use client'; import { useId, useMemo } from 'react'; import { Minus, Plus } from 'lucide-react'; import type { TlsCertificateItem } from '@/features/tls-certificates/types'; import { ResourceInput, ResourceSelect, SecondaryButton, } from '@/features/shared/components/resource-primitives'; export type DomainListRow = { domain: string; certificateId: string; }; const actionButtonBaseClassName = 'h-11 w-11 shrink-0 rounded-2xl px-0'; const removeButtonClassName = 'border-[var(--border-default)] bg-[var(--surface-elevated)] text-[var(--foreground-secondary)] hover:border-[var(--status-danger-border)] hover:bg-[var(--status-danger-soft)] hover:text-[var(--status-danger-foreground)] disabled:border-[var(--border-default)] disabled:bg-[var(--surface-muted)] disabled:text-[var(--foreground-muted)]'; const addButtonClassName = 'border-dashed border-[var(--border-default)] bg-[var(--surface-muted)] text-[var(--foreground-secondary)] hover:border-[var(--brand-primary)] hover:bg-[var(--brand-primary-soft)] hover:text-[var(--brand-primary)]'; function ensureRows(rows: DomainListRow[]) { return rows.length > 0 ? rows : [{ domain: '', certificateId: '' }]; } function buildSuggestionSources(domains: string[]) { const values = new Set(); for (const domain of domains) { const normalized = domain.trim().toLowerCase().replace(/^\*\./, ''); if (!normalized) { continue; } values.add(normalized); const segments = normalized.split('.'); for (let index = 1; index < segments.length - 1; index += 1) { values.add(segments.slice(index).join('.')); } } return Array.from(values); } function buildDomainSuggestions( input: string, sources: string[], rows: DomainListRow[], ) { const normalizedInput = input.trim().toLowerCase(); if (!normalizedInput) { return []; } const existingDomains = new Set( rows .map((row) => row.domain.trim().toLowerCase()) .filter((row) => row && row !== normalizedInput), ); const suggestions: string[] = []; for (const source of sources) { if (source.startsWith(normalizedInput) && source !== normalizedInput) { suggestions.push(source); } const separatorIndex = normalizedInput.lastIndexOf('.'); if (separatorIndex <= 0) { continue; } const prefix = normalizedInput.slice(0, separatorIndex); const suffixInput = normalizedInput.slice(separatorIndex + 1); if (!suffixInput || source.startsWith(suffixInput)) { suggestions.push(`${prefix}.${source}`); } } return suggestions.filter((suggestion, index) => { return ( suggestion !== normalizedInput && !existingDomains.has(suggestion) && suggestions.indexOf(suggestion) === index ); }); } export function buildDomainRowsFromRoute( domains: string[], domainCertIDs: number[], certIDs: number[], ): DomainListRow[] { if (domains.length === 0) { return ensureRows([]); } if (domainCertIDs.length === domains.length) { return domains.map((domain, index) => ({ domain, certificateId: domainCertIDs[index] ? String(domainCertIDs[index]) : '', })); } if (certIDs.length === 0) { return domains.map((domain) => ({ domain, certificateId: '' })); } if (certIDs.length === 1) { return domains.map((domain) => ({ domain, certificateId: String(certIDs[0]), })); } return domains.map((domain, index) => ({ domain, certificateId: certIDs[index] ? String(certIDs[index]) : '', })); } export function DomainListInput({ rows, onChange, onBlur, suggestionSources = [], certificates = [], domainPlaceholder = 'app.example.com', }: { rows: DomainListRow[]; onChange: (rows: DomainListRow[]) => void; onBlur?: () => void; suggestionSources?: string[]; certificates?: TlsCertificateItem[]; domainPlaceholder?: string; }) { const listId = useId(); const safeRows = ensureRows(rows); const normalizedSources = useMemo( () => buildSuggestionSources(suggestionSources), [suggestionSources], ); const updateRows = (nextRows: DomainListRow[]) => { onChange(ensureRows(nextRows)); }; return (
{safeRows.map((row, index) => { const suggestions = buildDomainSuggestions( row.domain, normalizedSources, safeRows, ).slice(0, 4); return (
{ if (safeRows.length === 1) { updateRows([{ domain: '', certificateId: '' }]); return; } updateRows( safeRows.filter((_, rowIndex) => rowIndex !== index), ); }} >
{ const nextRows = safeRows.slice(); nextRows[index] = { ...nextRows[index], domain: event.target.value, }; updateRows(nextRows); }} className="h-12" /> {suggestions.map((suggestion) => ( {suggestions.length > 0 ? (
{suggestions.map((suggestion) => ( ))}
) : null}
{ const nextRows = safeRows.slice(); nextRows[index] = { ...nextRows[index], certificateId: event.target.value, }; updateRows(nextRows); }} className="h-12" > {certificates.map((certificate) => ( ))}
); })} { updateRows([...safeRows, { domain: '', certificateId: '' }]); }} >
); }