449723e25f
The icon next to the list title adds visual noise without helping. The PartyPopper icon is kept for starter packs (kind 39089) where it has distinct meaning.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { Users, PartyPopper } from 'lucide-react';
|
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
|
import { getAvatarShape } from '@/lib/avatarShape';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { useAuthors } from '@/hooks/useAuthors';
|
|
import { genUserName } from '@/lib/genUserName';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
|
|
function getTag(tags: string[][], name: string): string | undefined {
|
|
return tags.find(([n]) => n === name)?.[1];
|
|
}
|
|
|
|
export function FollowPackContent({ event }: { event: NostrEvent }) {
|
|
const isFollowSet = event.kind === 30000;
|
|
const title = getTag(event.tags, 'title') || getTag(event.tags, 'name');
|
|
const description = getTag(event.tags, 'description') || getTag(event.tags, 'summary');
|
|
const image = getTag(event.tags, 'image');
|
|
const pubkeys = useMemo(
|
|
() => event.tags.filter(([n]) => n === 'p').map(([, pk]) => pk),
|
|
[event.tags],
|
|
);
|
|
|
|
// Only fetch first few avatars for the preview
|
|
const previewPubkeys = useMemo(() => pubkeys.slice(0, 8), [pubkeys]);
|
|
const { data: membersMap } = useAuthors(previewPubkeys);
|
|
|
|
return (
|
|
<div className="mt-2">
|
|
{/* Title */}
|
|
{title && (
|
|
<div className="flex items-center gap-2 mb-2">
|
|
{!isFollowSet && <PartyPopper className="size-4 text-primary shrink-0" />}
|
|
<span className="text-[15px] font-semibold leading-snug">{title}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description */}
|
|
{description && (
|
|
<p className="text-[15px] leading-relaxed text-foreground/90 line-clamp-3 mb-3">
|
|
{description}
|
|
</p>
|
|
)}
|
|
|
|
{/* Cover image */}
|
|
{image && (
|
|
<div className="rounded-2xl overflow-hidden mb-3">
|
|
<img
|
|
src={image}
|
|
alt={title ?? 'Follow pack'}
|
|
className="w-full max-h-[200px] object-cover"
|
|
loading="lazy"
|
|
onError={(e) => {
|
|
(e.currentTarget.parentElement as HTMLElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Member count pill + avatar stack */}
|
|
{pubkeys.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="text-[11px] gap-1 font-medium shrink-0">
|
|
<Users className="size-3" />
|
|
{pubkeys.length}
|
|
</Badge>
|
|
<div className="flex -space-x-2">
|
|
{previewPubkeys.map((pk) => {
|
|
const member = membersMap?.get(pk);
|
|
const name = member?.metadata?.name || genUserName(pk);
|
|
const shape = getAvatarShape(member?.metadata);
|
|
return (
|
|
<Avatar key={pk} shape={shape} className="size-7">
|
|
<AvatarImage src={member?.metadata?.picture} alt={name} />
|
|
<AvatarFallback className="bg-primary/20 text-primary text-[10px]">
|
|
{name[0]?.toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
);
|
|
})}
|
|
</div>
|
|
{pubkeys.length > previewPubkeys.length && (
|
|
<span className="text-xs text-muted-foreground">
|
|
+{pubkeys.length - previewPubkeys.length} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|