- lib/badges.ts + ProfileBadges: a profile shows (1) the built-in SkateSpots badge, derived from a nip05 ending @skate-spots.org (app logo linking to https://skate-spots.org/ — no storage, works for existing users), and (2) user badges from a kind-0 badges array ({label,url?}) rendered as clickable chips. - NIP19Page: app badge in the header, user badges in a 'Badges' card (replaces the plain Sponsors section). - EditProfileForm: Sponsors editor becomes a Badges editor (label + optional link); saves the badges array and retires the legacy newline sponsors field. Existing sponsors are read back as label-only badges, so nothing disappears.
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
/**
|
|
* 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<string, unknown>) | 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<string, unknown>;
|
|
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;
|
|
}
|