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 (
);
}
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 (
);
}
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);
}}
/>
)}
);
}