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 { 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 { 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 = `Yo — join the ${crew.name} crew on SkateSpots 🛹\n${crewUrl}\n\n` + `Log in with your Nostr key, then confirm your membership on the crew page.`; 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: 'Copied!' }); } catch { toast({ title: 'Copy failed', variant: 'destructive' }); } }; const nativeShare = async () => { if (navigator.share) { try { await navigator.share({ title: `Join ${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: 'Invite sent! 🛹', description: "They'll get a notification to accept or decline.", }); setInviteeInput(''); setNote(''); setAsAdmin(false); setOpen(false); } catch (err) { toast({ title: 'Could not send invite', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive', }); } }; return ( Invite to {crew.name} Send an invite to someone's Nostr key, or share a link. Send invite Share link {/* ── 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 && (

That doesn't look like a valid npub.

)}
{resolvedPubkey && }