import { useEffect, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { ArrowLeft, CreditCard, Mail, Shield, UserPlus, Users, X, } from 'lucide-react'; import { toast } from 'sonner'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Muted } from '@/components/ui/typography'; import { WindowControls } from '@/components/window-controls'; import { useNetworks } from '@/hooks/use-networks'; import { useNetworkInvitations, useRevokeInvitation, useRemoveMember, } from '@/hooks/use-member-management'; import { useAuthStore } from '@/stores/auth-store'; import { BillingSection } from '@/features/network-billing'; import { AddMembersDialog } from '@/features/network-settings/add-members-dialog'; import { ConfirmDestructiveOverlay } from '@/components/confirm-destructive-overlay'; import type { Human } from '@/api/types'; type Section = 'members' | 'billing'; function MemberRow({ human, isAdmin, onRemove, }: { human: Human; isAdmin: boolean; onRemove?: () => void; }) { const initials = human.email_prefix.slice(0, 2).toUpperCase(); return (
{initials}

{human.email_prefix}

{human.email}
{isAdmin && ( Admin )} {onRemove && ( )}
); } function PendingInvitationRow({ email, networkId, }: { email: string; networkId: string; }) { const revokeInvitation = useRevokeInvitation(networkId); const handleRevoke = () => { revokeInvitation.mutate(email, { onSuccess: () => { toast.success(`Invitation to ${email} revoked`); }, }); }; return (

{email}

Pending
); } function SectionHeading({ title, description, count, action, }: { title: string; description?: string; count?: number; action?: React.ReactNode; }) { return (

{title}

{count != null && ( {count} )}
{description && {description}}
{action}
); } function Panel({ children }: { children: React.ReactNode }) { return (
{children}
); } export default function NetworkSettingsPage() { const navigate = useNavigate(); const { networkId } = useParams<{ networkId: string }>(); if (!networkId) throw new Error('NetworkSettingsPage requires a :networkId route param'); const [searchParams, setSearchParams] = useSearchParams(); const { data: networks } = useNetworks(); const network = networks?.find((n) => n.id === networkId); const { data: invitations, error: invitationsError } = useNetworkInvitations(networkId); const currentUser = useAuthStore((s) => s.user); const isAdmin = currentUser?.id === network?.admin_human.id; const [memberToRemove, setMemberToRemove] = useState(null); // Onboarding: opening settings with `?add=1` (e.g. right after creating a // network) starts with the Add members dialog open. Non-admins never render // the dialog, so the initial value is harmless for them. const [addOpen, setAddOpen] = useState(() => searchParams.get('add') === '1'); const removeMember = useRemoveMember(networkId); const section: Section = searchParams.get('section') === 'billing' ? 'billing' : 'members'; const setSection = (value: string) => { const next = new URLSearchParams(searchParams); if (value === 'billing') next.set('section', value); else next.delete('section'); setSearchParams(next, { replace: true }); }; // Strip the one-shot `add` param so the dialog doesn't reopen on refresh or // back navigation. The initial open state was already captured above. useEffect(() => { if (searchParams.get('add') !== '1') return; setSearchParams( (prev) => { const next = new URLSearchParams(prev); next.delete('add'); return next; }, { replace: true }, ); }, [setSearchParams, searchParams]); const networkName = network?.name ?? 'Network'; const memberCount = network?.humans.length ?? 0; const pendingCount = invitations?.length ?? 0; const networkInitials = networkName.slice(0, 2).toUpperCase(); return (
Settings
setAddOpen(true)} > Add members ) : undefined } /> {network?.humans.map((human, index) => { const isRowAdmin = human.id === network.admin_human.id; const canRemove = isAdmin && !isRowAdmin && human.id !== currentUser?.id; return (
setMemberToRemove(human) : undefined } /> {index < network.humans.length - 1 && ( )}
); })}
{isAdmin && (
0 ? pendingCount : undefined} /> {invitationsError ? (

Couldn't load pending invitations.

) : invitations && invitations.length > 0 ? ( {invitations.map((inv, index) => (
{index < invitations.length - 1 && ( )}
))}
) : ( No pending invitations. )}
)}
{isAdmin && ( )} {memberToRemove && (
  • They'll lose access to this network's streams and files within seconds.
  • Any content they posted stays in the network.
  • If they're in a live huddle, they may remain until the call ends.
  • } confirmLabel="Remove" pendingLabel="Removing…" isPending={removeMember.isPending} onConfirm={() => { const target = memberToRemove; removeMember.mutate(target.id, { onSuccess: () => { toast.success(`Removed ${target.email}`); setMemberToRemove(null); }, }); }} onClose={() => { if (!removeMember.isPending) setMemberToRemove(null); }} /> )}
    ); }