import { useState } from 'react'; import { nip19 } from 'nostr-tools'; import { User, Users, X, Plus, ShieldCheck } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import type { EntityType } from '@/lib/types'; import { cn } from '@/lib/utils'; interface EntityTypeFieldProps { entityType: EntityType; onEntityTypeChange: (t: EntityType) => void; /** Member pubkeys (hex). */ members: string[]; onMembersChange: (members: string[]) => void; /** Accent color class set for the current category (e.g. red/blue/indigo). */ accentBg?: string; accentText?: string; } function MemberRow({ pubkey, onRemove }: { pubkey: string; onRemove: () => void }) { const author = useAuthor(pubkey); const metadata = author.data?.metadata; const name = metadata?.name ?? genUserName(pubkey); const npub = nip19.npubEncode(pubkey); return (
{name.slice(0, 2).toUpperCase()}

{name}

{npub.slice(0, 16)}…

); } /** * Individual / Collective toggle plus, for collectives, a member npub picker. * Members are stored as hex pubkeys. Membership only becomes "confirmed" once * each member publishes a NIP-32 attestation with their own key (handled on * the detail page). */ export function EntityTypeField({ entityType, onEntityTypeChange, members, onMembersChange, accentBg = 'bg-indigo-600', accentText = 'text-indigo-400', }: EntityTypeFieldProps) { const [input, setInput] = useState(''); const [error, setError] = useState(null); const addMember = () => { const raw = input.trim(); if (!raw) return; let hex: string | null = null; try { if (raw.startsWith('npub1') || raw.startsWith('nostr:npub1')) { const decoded = nip19.decode(raw.replace(/^nostr:/, '')); if (decoded.type === 'npub') hex = decoded.data; } else if (raw.startsWith('nprofile1')) { const decoded = nip19.decode(raw); if (decoded.type === 'nprofile') hex = decoded.data.pubkey; } else if (/^[0-9a-f]{64}$/i.test(raw)) { hex = raw.toLowerCase(); } } catch { hex = null; } if (!hex) { setError('Enter a valid npub, nprofile, or 64-char hex pubkey.'); return; } if (members.includes(hex)) { setError('That member is already added.'); return; } onMembersChange([...members, hex]); setInput(''); setError(null); }; return (
{entityType === 'collective' && (

Add the Nostr accounts of your crew. Each member confirms their own spot with their key — until then they show as “pending”.

{ setInput(e.target.value); setError(null); }} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addMember(); } }} spellCheck={false} autoCapitalize="none" className="bg-gray-800 border-gray-700 font-mono text-sm" />
{error &&

{error}

} {members.length > 0 && (
{members.map((pk) => ( onMembersChange(members.filter((m) => m !== pk))} /> ))}
)}

Membership is verified per member — nobody can be added to your crew without confirming from their own account.

)}
); }