feat: improve badges display with images and limited count (#128)

- Add badge image display to BadgeDefinitionRenderer feed items
  - Shows badge image/icon (16x16) with name and description
  - Falls back to Award icon if no image available
- Limit ProfileBadgesRenderer to show max 5 badges with "& n more" pattern
  - Prevents overcrowded feeds when users have many badges
  - Maintains clickability to see full list in detail view
- Rename "Badge definition" to "Badge" for clearer user-facing text
  - Updated constants/kinds.ts and nostr-kinds-schema.yaml
  - Simplifies terminology (kind 30009 is just "Badge" not "Badge definition")

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-17 21:41:05 +01:00
committed by GitHub
parent 9282e45910
commit c7cced2a9e
4 changed files with 47 additions and 19 deletions

View File

@@ -7,35 +7,55 @@ import {
getBadgeIdentifier,
getBadgeName,
getBadgeDescription,
getBadgeImageUrl,
} from "@/lib/nip58-helpers";
import { Award } from "lucide-react";
/**
* Renderer for Kind 30009 - Badge (NIP-58)
* Simple feed view with name and description
* Feed view with image, name and description
*/
export function BadgeDefinitionRenderer({ event }: BaseEventProps) {
const identifier = getBadgeIdentifier(event);
const name = getBadgeName(event);
const description = getBadgeDescription(event);
const imageUrl = getBadgeImageUrl(event);
// Use name if available, fallback to identifier
const displayTitle = name || identifier || "Badge";
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-1">
<ClickableEventTitle
event={event}
className="text-base font-semibold text-foreground"
>
{displayTitle}
</ClickableEventTitle>
{description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{description}
</p>
<div className="flex gap-3">
{/* Badge Image */}
{imageUrl ? (
<img
src={imageUrl}
alt={displayTitle}
className="size-16 rounded-lg object-cover flex-shrink-0"
loading="lazy"
/>
) : (
<div className="size-16 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
<Award className="size-8 text-muted-foreground" />
</div>
)}
{/* Badge Info */}
<div className="flex flex-col gap-1 flex-1 min-w-0">
<ClickableEventTitle
event={event}
className="text-base font-semibold text-foreground"
>
{displayTitle}
</ClickableEventTitle>
{description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{description}
</p>
)}
</div>
</div>
</BaseEventContainer>
);

View File

@@ -74,10 +74,13 @@ function BadgeItem({ badgeAddress }: { badgeAddress: string }) {
/**
* Renderer for Kind 30008 - Profile Badges (NIP-58)
* Shows all badge thumbnails, clickable to open detail view
* Shows limited badge thumbnails with "& n more" pattern, clickable to open detail view
*/
export function ProfileBadgesRenderer({ event }: BaseEventProps) {
const badgePairs = getProfileBadgePairs(event);
const MAX_VISIBLE_BADGES = 5;
const visibleBadges = badgePairs.slice(0, MAX_VISIBLE_BADGES);
const remainingCount = Math.max(0, badgePairs.length - MAX_VISIBLE_BADGES);
if (badgePairs.length === 0) {
return (
@@ -101,11 +104,16 @@ export function ProfileBadgesRenderer({ event }: BaseEventProps) {
{badgePairs.length} {badgePairs.length === 1 ? "badge" : "badges"}
</ClickableEventTitle>
{/* All Badge Thumbnails */}
{/* Limited Badge Thumbnails */}
<div className="flex items-center gap-2 flex-wrap">
{badgePairs.map((pair, idx) => (
{visibleBadges.map((pair, idx) => (
<BadgeItem key={idx} badgeAddress={pair.badgeAddress} />
))}
{remainingCount > 0 && (
<span className="text-sm text-muted-foreground">
& {remainingCount} more
</span>
)}
</div>
</div>
</BaseEventContainer>

View File

@@ -1096,8 +1096,8 @@ export const EVENT_KINDS: Record<number | string, EventKind> = {
},
30009: {
kind: 30009,
name: "Badge Definition",
description: "Badge Definition",
name: "Badge",
description: "Badge",
nip: "58",
icon: Award,
},

View File

@@ -1402,7 +1402,7 @@ kinds:
- *etag
30009:
description: Badge Definition
description: Badge
in_use: true
content:
type: free