import { useState } from 'react';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import {
useApproveJoinRequest,
useDismissJoinRequest,
useEndorseJoinRequest,
} from '@/hooks/useCrewInvites';
import { useToast } from '@/hooks/useToast';
import { genUserName } from '@/lib/genUserName';
import { parseAddrCoord, type CrewEndorsement, type CrewJoinRequest, type SkateCrew } from '@/lib/types';
import { UserPlus, Check, X, Loader2, Hand, ThumbsUp, Clock } from 'lucide-react';
import { cn } from '@/lib/utils';
function npubSafe(pubkey: string): string {
try {
return nip19.npubEncode(pubkey);
} catch {
return pubkey;
}
}
/** Tiny avatar for an endorsing admin (used in the vouch strip). */
function EndorserAvatar({ pubkey }: { pubkey: string }) {
const author = useAuthor(pubkey);
const name = author.data?.metadata?.name ?? genUserName(pubkey);
return (
{name.slice(0, 2).toUpperCase()}
);
}
/** Readable "Vouched by Rider, Mia +1" line for the endorsement strip. */
function EndorserNames({ pubkeys }: { pubkeys: string[] }) {
const first = useAuthor(pubkeys[0]);
const firstName = first.data?.metadata?.name ?? genUserName(pubkeys[0]);
const extra = pubkeys.length - 1;
return (
{firstName}
{extra > 0 && +{extra} more}
);
}
/**
* A single incoming join-request card for a crew's founder / admins.
*
* Trust model: only the founder authors the crew event, so only they can *add*
* a member. Admins therefore **endorse** ("vouch for") a request; the founder
* sees those endorsements as social proof and ratifies with one click. This
* closes the admin/founder asymmetry without breaking who-can-edit-the-roster.
*/
export function JoinRequestCard({
request,
crew,
endorsements = [],
onResolved,
compact,
}: {
request: CrewJoinRequest;
crew: SkateCrew;
endorsements?: CrewEndorsement[];
onResolved?: () => void;
compact?: boolean;
}) {
const { user } = useCurrentUser();
const requester = useAuthor(request.requester);
const requesterName = requester.data?.metadata?.name ?? genUserName(request.requester);
const requesterPic = requester.data?.metadata?.picture;
const { toast } = useToast();
const { mutateAsync: approve, isPending: approving } = useApproveJoinRequest();
const { mutateAsync: dismiss, isPending: dismissing } = useDismissJoinRequest();
const { mutateAsync: endorse, isPending: endorsing } = useEndorseJoinRequest();
const [done, setDone] = useState<'approved' | 'dismissed' | 'endorsed' | null>(null);
const coord = parseAddrCoord(request.crewCoord);
const crewHref = coord ? `/crew/${coord.pubkey}/${coord.identifier}` : '/crews';
const meLc = user?.pubkey.toLowerCase();
const isFounder = !!meLc && meLc === crew.pubkey.toLowerCase();
const isAdmin = !!meLc && crew.admins.some((a) => a.toLowerCase() === meLc);
const endorserPubkeys = endorsements.map((e) => e.endorser);
const iEndorsed = !!meLc && endorserPubkeys.some((p) => p.toLowerCase() === meLc);
const hasEndorsements = endorserPubkeys.length > 0;
const handleApprove = async () => {
try {
await approve({ request, crew });
setDone('approved');
toast({
title: `Added ${requesterName} to ${crew.name} πΉ`,
description: "They'll confirm from their side to appear as a member.",
});
onResolved?.();
} catch (err) {
toast({
title: 'Could not add member',
description: err instanceof Error ? err.message : 'Please try again.',
variant: 'destructive',
});
}
};
const handleEndorse = async () => {
try {
await endorse({ request, crew });
setDone('endorsed');
toast({
title: `You vouched for ${requesterName} π€`,
description: `The founder of ${crew.name} will see your endorsement and can add them.`,
});
onResolved?.();
} catch (err) {
toast({
title: 'Could not endorse',
description: err instanceof Error ? err.message : 'Please try again.',
variant: 'destructive',
});
}
};
const handleDismiss = async () => {
try {
await dismiss({ request });
setDone('dismissed');
onResolved?.();
} catch {
toast({ title: 'Could not dismiss', variant: 'destructive' });
}
};
if (done) {
return (
{done === 'approved' ? (
<>
Added {requesterName} to {crew.name}
>
) : done === 'endorsed' ? (
<>
You vouched for {requesterName} β founder will confirm
>
) : (
<>
Dismissed request from {requesterName}
>
)}
);
}
const busy = approving || dismissing || endorsing;
return (
{requesterName.slice(0, 2).toUpperCase()}
{requesterName}
{' '}
wants to join{' '}
{crew.name}
{request.note && (
β{request.note}β
)}
{/* Vouch strip β admins' endorsements as social proof for the founder. */}
{hasEndorsements && (
{endorserPubkeys.slice(0, 3).map((pk) => (
))}
Vouched by
)}
{isFounder ? (
) : isAdmin ? (
iEndorsed ? (
Vouched β waiting on the founder
) : (
)
) : (
Crew leadership only.
)}
{/* Micro-help so the admin path isn't a mystery. */}
{isAdmin && !iEndorsed && (
Admins canβt add members directly β vouching tells the founder you back this rider.
)}
);
}