/** * Profile badges — the small logos/chips on a profile. * * Two sources: * 1. The built-in **SkateSpots** badge: any handle issued by our own domain * (`name@skate-spots.org`) earns the app-logo badge, linking to the site. * Derived — never stored — so it works for every existing user. * 2. User badges: a `badges` array in the kind-0 metadata * (`{ label, url? }`), e.g. sponsors a skater represents. Clicking one opens * the link the skater chose. * * Back-compat: the old `sponsors` newline-string is carried forward as * label-only badges, so nothing disappears for people who set it before. */ import type { NostrMetadata } from '@nostrify/nostrify'; export interface ProfileBadge { label: string; /** Optional link the badge opens. */ url?: string; } export const SKATESPOTS_SITE = 'https://skate-spots.org/'; /** The app badge every `@skate-spots.org` handle earns (logo → the site). */ export const SKATESPOTS_BADGE: ProfileBadge = { label: 'SkateSpots', url: SKATESPOTS_SITE, }; /** True when a handle is issued by our own domain. */ export function isSkateSpotsNip05(nip05?: string | null): boolean { return !!nip05 && nip05.trim().toLowerCase().endsWith('@skate-spots.org'); } /** A safe external link, or undefined. */ function safeUrl(value: unknown): string | undefined { if (typeof value !== 'string') return undefined; const url = value.trim(); return /^https?:\/\//i.test(url) ? url : undefined; } /** Parse the profile's own badges from kind-0 metadata (defensive). */ export function parseBadges(metadata: NostrMetadata | undefined): ProfileBadge[] { const raw = metadata as (NostrMetadata & Record) | undefined; const out: ProfileBadge[] = []; if (Array.isArray(raw?.badges)) { for (const entry of raw.badges) { if (!entry || typeof entry !== 'object') continue; const { label, url } = entry as Record; if (typeof label !== 'string' || !label.trim()) continue; out.push({ label: label.trim().slice(0, 40), url: safeUrl(url) }); if (out.length >= 12) break; } } // Legacy `sponsors` (newline-separated) → label-only badges. if (typeof raw?.sponsors === 'string') { for (const line of raw.sponsors.split('\n')) { const label = line.trim(); if (!label || out.some((b) => b.label === label)) continue; out.push({ label: label.slice(0, 40) }); if (out.length >= 12) break; } } return out; } /** The badges to render for a profile: the app badge first, then the user's own. */ export function profileBadges(metadata: NostrMetadata | undefined): ProfileBadge[] { const own = parseBadges(metadata); const nip05 = (metadata as { nip05?: string } | undefined)?.nip05; return isSkateSpotsNip05(nip05) ? [SKATESPOTS_BADGE, ...own] : own; }