SkateSpots/src/components/ProfileBadges.tsx
SkateSpots 5f5a954ce6 feat(profile): badges — clickable chips + a SkateSpots member badge
- 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.
2026-09-28 19:40:19 +07:00

67 lines
2 KiB
TypeScript

import { BadgeCheck, ExternalLink } from 'lucide-react';
import { cn } from '@/lib/utils';
import { SKATESPOTS_SITE, type ProfileBadge } from '@/lib/badges';
/** The app logo is already served with the site. */
const SKATESPOTS_LOGO = '/images/logo-icon.png';
const isApp = (b: ProfileBadge) => b.label === 'SkateSpots' && b.url === SKATESPOTS_SITE;
/**
* ProfileBadges — the row of badges on a profile (own or someone else's).
* Clicking a badge with a link opens it in a new tab.
*/
export function ProfileBadges({
badges,
size = 'md',
className,
}: {
badges: ProfileBadge[];
size?: 'sm' | 'md';
className?: string;
}) {
if (badges.length === 0) return null;
return (
<div className={cn('flex flex-wrap items-center gap-2', className)}>
{badges.map((b, i) => {
const app = isApp(b);
const cls = cn(
'inline-flex items-center gap-1.5 rounded-full border font-bold transition-colors',
size === 'sm' ? 'px-2 py-0.5 text-[11px]' : 'px-3 py-1.5 text-sm',
app
? 'border-[#ccff00]/40 bg-[#ccff00]/10 text-[#ccff00] hover:bg-[#ccff00]/20'
: 'border-pink-500/25 bg-pink-500/10 text-gray-100 hover:bg-pink-500/20',
);
const inner = (
<>
{app ? (
<img src={SKATESPOTS_LOGO} alt="" className="h-4 w-4 rounded-full object-contain" />
) : (
<BadgeCheck className={size === 'sm' ? 'h-3 w-3' : 'h-3.5 w-3.5'} />
)}
<span className="max-w-[12rem] truncate">{b.label}</span>
{b.url && !app && <ExternalLink className="h-3 w-3 opacity-60" />}
</>
);
return b.url ? (
<a
key={`${b.label}-${i}`}
href={b.url}
target="_blank"
rel="noopener noreferrer"
title={b.url}
className={cls}
>
{inner}
</a>
) : (
<span key={`${b.label}-${i}`} className={cls}>
{inner}
</span>
);
})}
</div>
);
}