import { useState } from 'react'; import { Link } from 'react-router-dom'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { useAuthor } from '@/hooks/useAuthor'; import { useAcceptCrewInvite, useDeclineCrewInvite } from '@/hooks/useCrewInvites'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { genUserName } from '@/lib/genUserName'; import { parseAddrCoord, type CrewInvite } from '@/lib/types'; import { Users, Check, X, Loader2, ShieldCheck } from 'lucide-react'; import { cn } from '@/lib/utils'; /** * A single crew-invite notification card with Accept / Decline actions. * Accepting publishes the invitee's membership attestation (making them a * confirmed member); declining dismisses the invite locally. */ export function CrewInviteCard({ invite, onResolved, compact, }: { invite: CrewInvite; onResolved?: () => void; compact?: boolean; }) { const { t } = useTranslation(); const inviter = useAuthor(invite.inviter); const inviterName = inviter.data?.metadata?.name ?? genUserName(invite.inviter); const inviterPic = inviter.data?.metadata?.picture; const { toast } = useToast(); const { mutateAsync: accept, isPending: accepting } = useAcceptCrewInvite(); const { mutateAsync: decline, isPending: declining } = useDeclineCrewInvite(); const [done, setDone] = useState<'accepted' | 'declined' | null>(null); const coord = parseAddrCoord(invite.crewCoord); const crewHref = coord ? `/crew/${coord.pubkey}/${coord.identifier}` : '/crews'; const handleAccept = async () => { try { await accept({ invite }); setDone('accepted'); toast({ title: t('socialUi.inviteCard.acceptedTitle'), description: t('socialUi.inviteCard.acceptedDesc', { crew: invite.crewName }), }); onResolved?.(); } catch (err) { toast({ title: t('socialUi.inviteCard.couldNotAccept'), description: err instanceof Error ? err.message : t('common.tryAgain'), variant: 'destructive', }); } }; const handleDecline = async () => { try { await decline({ invite }); setDone('declined'); onResolved?.(); } catch { toast({ title: t('socialUi.inviteCard.couldNotDecline'), variant: 'destructive' }); } }; if (done) { return (
{done === 'accepted' ? ( <> {t('socialUi.inviteCard.acceptedDone')} {invite.crewName} {t('socialUi.inviteCard.confirmsMembers')} ) : ( <> {t('socialUi.inviteCard.declinedDone', { crew: invite.crewName })} )}
); } const busy = accepting || declining; return (

{inviterName.slice(0, 2).toUpperCase()} {inviterName} {' '} {t('socialUi.inviteCard.invitedYou')}{' '} {invite.crewName} {invite.role === 'admin' && ( {t('socialUi.inviteCard.admin')} )}

{invite.note && (

“{invite.note}”

)}
); } // Keep a tiny local encoder to avoid importing nip19 into every card render path. import { nip19 } from 'nostr-tools'; function inviterNpubSafe(pubkey: string): string { try { return nip19.npubEncode(pubkey); } catch { return pubkey; } }