Step 1 of the collective plan (shared primitive on existing Lab categories): - types.ts: EntityType + entity/members fields on coach/filmer/editor/ photographer interfaces & parsers; membership label namespace constants + parseMemberPubkeys / parseEntityType helpers - useCollectiveMembers: cross-references claimed `p` tags against members' own NIP-32 (kind 1985) attestations -> confirmed / pending status - useConfirmMembership: publishes a member's self-signed membership label - EntityTypeField: individual/collective toggle + member npub picker (per- category accent) wired into all 4 Become-* forms - CollectiveMembers: sidebar card with confirmed/pending trust chips - ConfirmMembershipCard: one-tap confirm shown only to listed pending members - CoachDetailPage: collective badge, members card, confirm-membership prompt - Lab cards: compact collective badge with member count - NIP.md: document entity tag + collective membership attestation flow Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { Users, Check, Loader2 } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useCollectiveMembers } from '@/hooks/useCollectiveMembers';
|
|
import { useConfirmMembership } from '@/hooks/useMembershipAttestation';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface ConfirmMembershipCardProps {
|
|
kind: number;
|
|
founderPubkey: string;
|
|
identifier: string;
|
|
collectiveName: string;
|
|
claimedMembers: string[];
|
|
accentBg?: string;
|
|
}
|
|
|
|
/**
|
|
* Shown to a logged-in user who has been LISTED as a member of a collective but
|
|
* has not yet confirmed. Publishes a NIP-32 attestation on click. Hidden for
|
|
* everyone else (non-members, already-confirmed members, the founder).
|
|
*/
|
|
export function ConfirmMembershipCard({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
collectiveName,
|
|
claimedMembers,
|
|
accentBg = 'bg-indigo-600 hover:bg-indigo-700',
|
|
}: ConfirmMembershipCardProps) {
|
|
const { user } = useCurrentUser();
|
|
const { toast } = useToast();
|
|
const { mutateAsync: confirm, isPending } = useConfirmMembership();
|
|
const { data: members } = useCollectiveMembers({
|
|
kind,
|
|
founderPubkey,
|
|
identifier,
|
|
claimedMembers,
|
|
enabled: claimedMembers.length > 0,
|
|
});
|
|
|
|
if (!user) return null;
|
|
|
|
const me = user.pubkey.toLowerCase();
|
|
// Only relevant if I'm actually listed as a member and I'm not the founder.
|
|
if (me === founderPubkey.toLowerCase()) return null;
|
|
if (!claimedMembers.includes(me)) return null;
|
|
|
|
const myStatus = members?.find((m) => m.pubkey === me)?.status;
|
|
if (myStatus === 'confirmed') return null;
|
|
|
|
const handleConfirm = async () => {
|
|
try {
|
|
await confirm({ kind, founderPubkey, identifier, collectiveName });
|
|
toast({
|
|
title: 'Membership confirmed!',
|
|
description: `You now show as a verified member of ${collectiveName}.`,
|
|
});
|
|
} catch (error) {
|
|
toast({
|
|
title: 'Failed to confirm',
|
|
description: error instanceof Error ? error.message : 'Please try again.',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="border-2 border-amber-500/50 bg-amber-950/20">
|
|
<CardContent className="space-y-3 p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Users className="mt-0.5 h-6 w-6 shrink-0 text-amber-400" />
|
|
<div>
|
|
<h3 className="font-bold text-amber-300">You've been added to {collectiveName}</h3>
|
|
<p className="text-sm text-gray-300">
|
|
Confirm your membership so it shows as verified on your profile and theirs.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
disabled={isPending}
|
|
className={cn('w-full gap-2 font-bold', accentBg)}
|
|
>
|
|
{isPending ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" /> Confirming…
|
|
</>
|
|
) : (
|
|
<>
|
|
<Check className="h-4 w-4" /> Confirm Membership
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|