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 { useTranslation } from '@/contexts/language';
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 { t } = useTranslation();
const first = useAuthor(pubkeys[0]);
const firstName = first.data?.metadata?.name ?? genUserName(pubkeys[0]);
const extra = pubkeys.length - 1;
return (
{firstName}
{extra > 0 && {t('socialUi.joinRequest.moreCount', { count: extra })}}
);
}
/**
* 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 { t } = useTranslation();
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: t('socialUi.joinRequest.addedTitle', { name: requesterName, crew: crew.name }),
description: t('socialUi.joinRequest.addedDesc'),
});
onResolved?.();
} catch (err) {
toast({
title: t('socialUi.collectiveJoinRequest.couldNotAdd'),
description: err instanceof Error ? err.message : t('common.tryAgain'),
variant: 'destructive',
});
}
};
const handleEndorse = async () => {
try {
await endorse({ request, crew });
setDone('endorsed');
toast({
title: t('socialUi.joinRequest.vouchedTitle', { name: requesterName }),
description: t('socialUi.joinRequest.vouchedDesc', { crew: crew.name }),
});
onResolved?.();
} catch (err) {
toast({
title: t('socialUi.joinRequest.couldNotEndorse'),
description: err instanceof Error ? err.message : t('common.tryAgain'),
variant: 'destructive',
});
}
};
const handleDismiss = async () => {
try {
await dismiss({ request });
setDone('dismissed');
onResolved?.();
} catch {
toast({ title: t('socialUi.collectiveJoinRequest.couldNotDismiss'), variant: 'destructive' });
}
};
if (done) {
return (
{done === 'approved' ? (
<>
{t('socialUi.collectiveJoinRequest.addedDone')} {requesterName} {t('socialUi.collectiveJoinRequest.addedTo')} {crew.name}
>
) : done === 'endorsed' ? (
<>
{t('socialUi.joinRequest.endorsedDoneA')} {requesterName} {t('socialUi.joinRequest.endorsedDoneB')}
>
) : (
<>
{t('socialUi.collectiveJoinRequest.dismissedDone', { name: requesterName })}
>
)}
);
}
const busy = approving || dismissing || endorsing;
return (
{requesterName.slice(0, 2).toUpperCase()}
{requesterName}
{' '}
{t('socialUi.collectiveJoinRequest.wantsToJoin')}{' '}
{crew.name}
{request.note && (
“{request.note}”
)}
{/* Vouch strip — admins' endorsements as social proof for the founder. */}
{hasEndorsements && (
{endorserPubkeys.slice(0, 3).map((pk) => (
))}
{t('socialUi.joinRequest.vouchedBy')}
)}
{isFounder ? (
) : isAdmin ? (
iEndorsed ? (
{t('socialUi.joinRequest.vouchedWaiting')}
) : (
)
) : (
{t('socialUi.joinRequest.leadershipOnly')}
)}
{/* Micro-help so the admin path isn't a mystery. */}
{isAdmin && !iEndorsed && (
{t('socialUi.joinRequest.adminHelp')}
)}
);
}