'use client'; import { useTranslations } from 'next-intl'; import { useEffect, useMemo, useState } from 'react'; import { Check, ChevronDown, Search } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { cn } from '@/lib/utils'; import type { CloudflareAvailableDomain } from '@/lib/services/openflare'; type DomainZoneGroup = { zoneId: number; zoneDomain: string; domains: CloudflareAvailableDomain[]; }; function groupDomainsByZone( domains: CloudflareAvailableDomain[], ): DomainZoneGroup[] { const map = new Map(); for (const domain of domains) { const existing = map.get(domain.zone_id); if (existing) { existing.domains.push(domain); continue; } map.set(domain.zone_id, { zoneId: domain.zone_id, zoneDomain: domain.zone_domain || `Zone #${domain.zone_id}`, domains: [domain], }); } return [...map.values()] .map((group) => ({ ...group, domains: [...group.domains].sort((a, b) => a.domain.localeCompare(b.domain), ), })) .sort((a, b) => a.zoneDomain.localeCompare(b.zoneDomain)); } export function MemberAddDialog({ open, onOpenChange, domains, defaultProxied, pending, onSubmit, }: { open: boolean; onOpenChange: (open: boolean) => void; domains: CloudflareAvailableDomain[]; defaultProxied: boolean; pending: boolean; onSubmit: (zoneDomainIDs: number[], proxied: boolean) => void; }) { const t = useTranslations('cloudflare.memberDialog'); const tCommon = useTranslations('common'); const [keyword, setKeyword] = useState(''); const [selectedIDs, setSelectedIDs] = useState([]); const [proxied, setProxied] = useState(defaultProxied); const [collapsedZones, setCollapsedZones] = useState>( () => new Set(), ); useEffect(() => { if (!open) return; setKeyword(''); setSelectedIDs([]); setProxied(defaultProxied); setCollapsedZones(new Set()); }, [defaultProxied, open]); const selected = useMemo(() => new Set(selectedIDs), [selectedIDs]); const filteredGroups = useMemo(() => { const normalized = keyword.trim().toLowerCase(); const list = normalized ? domains.filter((domain) => { const zoneRoot = domain.zone_domain ?? ''; return ( domain.domain.toLowerCase().includes(normalized) || zoneRoot.toLowerCase().includes(normalized) || String(domain.id).includes(normalized) ); }) : domains; return groupDomainsByZone(list); }, [domains, keyword]); const visibleIDs = useMemo( () => filteredGroups.flatMap((group) => group.domains.map((d) => d.id)), [filteredGroups], ); const allVisibleSelected = visibleIDs.length > 0 && visibleIDs.every((id) => selected.has(id)); const someVisibleSelected = visibleIDs.some((id) => selected.has(id)) && !allVisibleSelected; const toggleOne = (id: number) => { setSelectedIDs((prev) => prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], ); }; const toggleGroup = (group: DomainZoneGroup) => { const ids = group.domains.map((d) => d.id); const allSelected = ids.every((id) => selected.has(id)); setSelectedIDs((prev) => { if (allSelected) { return prev.filter((id) => !ids.includes(id)); } const next = new Set(prev); for (const id of ids) next.add(id); return [...next]; }); }; const toggleAllVisible = () => { setSelectedIDs((prev) => { if (allVisibleSelected) { return prev.filter((id) => !visibleIDs.includes(id)); } const next = new Set(prev); for (const id of visibleIDs) next.add(id); return [...next]; }); }; const toggleCollapsed = (zoneId: number) => { setCollapsedZones((prev) => { const next = new Set(prev); if (next.has(zoneId)) next.delete(zoneId); else next.add(zoneId); return next; }); }; return ( {t('title')} {t('description')} {t('zoneDomain')}
setKeyword(event.target.value)} placeholder={t('searchPlaceholder')} className='pl-8' disabled={pending} />
{t('selected', { count: selectedIDs.length })} {t('visible', { count: visibleIDs.length })}
{domains.length === 0 ? (
{t('noAvailable')}
) : filteredGroups.length === 0 ? (
{t('noMatch')}
) : (
{/* Header row: select all visible */} {filteredGroups.map((group) => { const groupIDs = group.domains.map((d) => d.id); const groupAllSelected = groupIDs.every((id) => selected.has(id), ); const groupSomeSelected = groupIDs.some((id) => selected.has(id)) && !groupAllSelected; const open = !collapsedZones.has(group.zoneId); return ( toggleCollapsed(group.zoneId)} >
toggleGroup(group)} aria-label={t('selectZone', { name: group.zoneDomain, })} className='ml-1' />
{group.domains.map((domain) => { const checked = selected.has(domain.id); const isApex = domain.domain === group.zoneDomain; return ( ); })}
); })}
)}
{t('proxied')}
); }