- Adopt NIP-52 RSVP (kind 31925): parseEventRsvp + RsvpStatus types - useEventRsvps (counts by status, attendee pubkeys, my status) + useRsvp mutation that replaces the user's prior RSVP via a stable d-tag - RsvpButtons (Going / Interested / Can't make it) + AttendeeStack avatar row - EventDetailPage: "Who's going" card with RSVP control + attendee avatars - EventCard: going count on both compact and full cards - EventsPage: quick date-window chips (Today / Weekend / Week / Month) - Activity bell: host RSVP notifications (new 'rsvp' activity type) - Document RSVP schema in NIP.md Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
71 lines
1.9 KiB
TypeScript
71 lines
1.9 KiB
TypeScript
import { Link } from 'react-router-dom';
|
|
import { nip19 } from 'nostr-tools';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { useAuthor } from '@/hooks/useAuthor';
|
|
import { genUserName } from '@/lib/genUserName';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
function StackAvatar({ pubkey, index }: { pubkey: string; index: number }) {
|
|
const author = useAuthor(pubkey);
|
|
const name = author.data?.metadata?.name ?? genUserName(pubkey);
|
|
const pic = author.data?.metadata?.picture;
|
|
|
|
const npub = (() => {
|
|
try {
|
|
return nip19.npubEncode(pubkey);
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
|
|
const avatar = (
|
|
<Avatar
|
|
className="h-7 w-7 border-2 border-background ring-0"
|
|
style={{ zIndex: 20 - index }}
|
|
title={name}
|
|
>
|
|
<AvatarImage src={pic} alt={name} />
|
|
<AvatarFallback className="text-[9px] font-bold">
|
|
{name.slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
);
|
|
|
|
if (!npub) return avatar;
|
|
return (
|
|
<Link to={`/${npub}`} className="transition-transform hover:-translate-y-0.5">
|
|
{avatar}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A horizontal, overlapping stack of attendee avatars with a "+N" overflow.
|
|
* Purely presentational — pass a list of pubkeys.
|
|
*/
|
|
export function AttendeeStack({
|
|
pubkeys,
|
|
max = 5,
|
|
className,
|
|
}: {
|
|
pubkeys: string[];
|
|
max?: number;
|
|
className?: string;
|
|
}) {
|
|
if (pubkeys.length === 0) return null;
|
|
const shown = pubkeys.slice(0, max);
|
|
const overflow = pubkeys.length - shown.length;
|
|
|
|
return (
|
|
<div className={cn('flex items-center', className)}>
|
|
<div className="flex -space-x-2">
|
|
{shown.map((pk, i) => (
|
|
<StackAvatar key={pk} pubkey={pk} index={i} />
|
|
))}
|
|
</div>
|
|
{overflow > 0 && (
|
|
<span className="ml-2 text-xs font-bold text-muted-foreground">+{overflow}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|