import { useState } from 'react'; import { UserPlus, Copy, Check, Share2, Edit, Send, Loader2, ShieldCheck } from 'lucide-react'; import { Link } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { useAuthor } from '@/hooks/useAuthor'; import { useSendCrewInvite } from '@/hooks/useCrewInvites'; import { genUserName } from '@/lib/genUserName'; import { cn } from '@/lib/utils'; import type { SkateCrew } from '@/lib/types'; interface CrewInviteDialogProps { crew: SkateCrew; /** Whether the viewer is the founder (gets the extra "add member" edit link + admin toggle). */ isFounder: boolean; triggerClassName?: string; } /** Try to resolve a raw string (npub / nprofile / hex) to a hex pubkey. */ function resolvePubkey(input: string): string | null { const trimmed = input.trim(); if (!trimmed) return null; // Bare hex if (/^[0-9a-f]{64}$/i.test(trimmed)) return trimmed.toLowerCase(); try { const decoded = nip19.decode(trimmed); if (decoded.type === 'npub') return decoded.data.toLowerCase(); if (decoded.type === 'nprofile') return decoded.data.pubkey.toLowerCase(); } catch { return null; } return null; } /** Small live preview of who a pubkey resolves to (avatar + name). */ function InviteePreview({ pubkey }: { pubkey: string }) { const author = useAuthor(pubkey); const name = author.data?.metadata?.name ?? genUserName(pubkey); const pic = author.data?.metadata?.picture; return (
{name.slice(0, 2).toUpperCase()}

{name}

{nip19.npubEncode(pubkey)}

); } /** * Invite flow for crew moderators (founder + admins). * * Two ways to invite: * 1. **Send invite** — publishes a structured invite (kind 36229) addressed to a * specific npub. That person gets a notification with Accept / Decline. * When the FOUNDER sends it, the invitee is pre-listed so accepting instantly * confirms membership. Admins can invite too (founder auto-confirms on accept). * 2. **Share link** — copy/share a link for people not yet on Nostr. */ export function CrewInviteDialog({ crew, isFounder, triggerClassName }: CrewInviteDialogProps) { const { t } = useTranslation(); const { toast } = useToast(); const [open, setOpen] = useState(false); const [copiedLink, setCopiedLink] = useState(false); const [copiedMsg, setCopiedMsg] = useState(false); // Send-invite state const [inviteeInput, setInviteeInput] = useState(''); const [note, setNote] = useState(''); const [asAdmin, setAsAdmin] = useState(false); const { mutateAsync: sendInvite, isPending: sending } = useSendCrewInvite(); const resolvedPubkey = resolvePubkey(inviteeInput); const inputLooksLikeKey = inviteeInput.trim().length > 0; const invalidKey = inputLooksLikeKey && !resolvedPubkey; const crewUrl = `${window.location.origin}/crew/${crew.pubkey}/${crew.identifier}`; const inviteMessage = `${t('socialUi.inviteDialog.messageIntro', { crew: crew.name })}\n${crewUrl}\n\n` + `${t('socialUi.inviteDialog.messageBody')}`; const copy = async (text: string, which: 'link' | 'msg') => { try { await navigator.clipboard.writeText(text); if (which === 'link') { setCopiedLink(true); setTimeout(() => setCopiedLink(false), 2000); } else { setCopiedMsg(true); setTimeout(() => setCopiedMsg(false), 2000); } toast({ title: t('common.copied') }); } catch { toast({ title: t('socialUi.inviteDialog.copyFailed'), variant: 'destructive' }); } }; const nativeShare = async () => { if (navigator.share) { try { await navigator.share({ title: t('socialUi.inviteDialog.shareTitle', { crew: crew.name }), text: inviteMessage, url: crewUrl }); } catch { /* cancelled */ } } else { copy(inviteMessage, 'msg'); } }; const handleSend = async () => { if (!resolvedPubkey) return; try { await sendInvite({ crew, invitee: resolvedPubkey, role: asAdmin ? 'admin' : 'member', note, isFounder, }); toast({ title: t('socialUi.inviteDialog.sentTitle'), description: t('socialUi.inviteDialog.sentDesc'), }); setInviteeInput(''); setNote(''); setAsAdmin(false); setOpen(false); } catch (err) { toast({ title: t('socialUi.inviteDialog.couldNotSend'), description: err instanceof Error ? err.message : t('common.tryAgain'), variant: 'destructive', }); } }; return ( {t('socialUi.inviteDialog.title', { crew: crew.name })} {t('socialUi.inviteDialog.description')} {t('socialUi.inviteDialog.sendTab')} {t('socialUi.inviteDialog.shareTab')} {/* ── Send a structured invite to an npub ── */}
setInviteeInput(e.target.value)} placeholder="npub1… or nprofile1…" className={cn('font-mono text-xs', invalidKey && 'border-red-500/50')} autoComplete="off" spellCheck={false} /> {invalidKey && (

{t('socialUi.inviteDialog.invalidNpub')}

)}
{resolvedPubkey && }