feat(supporters): top 21, extravagant auto tiers + tier ladder with supporter avatars

This commit is contained in:
SkateSpots 2026-10-05 19:02:25 +07:00
parent 66d83123cd
commit cc1883cf82
3 changed files with 85 additions and 16 deletions

View file

@ -6501,6 +6501,7 @@ walletUi: {
supportersTotals: '{{count}} supporters · {{angor}} sats via Angor · {{zap}} sats zapped',
quietNote: 'Plus everyone who gave quietly — thank you. ❤️',
tiersNote: 'Tiers: Supporter under 100k sats · Patron 100k–1M · Legend 1M+',
tierEmpty: 'No one yet — be the first.',
joinSupporters: 'Join {{count}} supporters',
showAll: 'Show all {{count}}',
showLess: 'Show less',

View file

@ -6501,6 +6501,7 @@ walletUi: {
supportersTotals: '{{count}} patrocinadores · {{angor}} sats vía Angor · {{zap}} sats zapeados',
quietNote: 'Y a todos los que aportaron en silencio — gracias. ❤️',
tiersNote: 'Niveles: Colaborador menos de 100k sats · Patrón 100k–1M · Leyenda 1M+',
tierEmpty: 'Nadie todavía — sé el primero.',
joinSupporters: 'Únete a {{count}} patrocinadores',
showAll: 'Ver los {{count}}',
showLess: 'Ver menos',

View file

@ -20,7 +20,7 @@ const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa0
// platform zap is already ≥ 2026-01, so this doesn't currently trim the list —
// the visible cap below is what keeps it short.
const SUPPORTERS_SINCE = Math.floor(Date.UTC(2025, 11, 1) / 1000); // 2025-12-01
const VISIBLE_LIMIT = 10;
const VISIBLE_LIMIT = 21;
// ─── Static data — edit these arrays to add/remove entries ──────────────────
@ -248,11 +248,77 @@ interface SupporterEntry {
}
const TIER_META = {
supporter: { color: '#22d3ee', key: 'walletUi.supporters.tierSupporter' },
patron: { color: '#f7931a', key: 'walletUi.supporters.tierPatron' },
legend: { color: '#facc15', key: 'walletUi.supporters.tierLegend' },
supporter: { color: '#22d3ee', key: 'walletUi.supporters.tierSupporter', emoji: '🛹' },
patron: { color: '#f7931a', key: 'walletUi.supporters.tierPatron', emoji: '🔥' },
legend: { color: '#facc15', key: 'walletUi.supporters.tierLegend', emoji: '👑💎✨' },
} as const;
const TIER_ORDER = ['legend', 'patron', 'supporter'] as const;
const TIER_RANGE: Record<(typeof TIER_ORDER)[number], string> = {
legend: '1M+',
patron: '100k–1M',
supporter: '< 100k',
};
function MemberAvatar({ entry }: { entry: SupporterEntry }) {
const { data } = useAuthor(entry.anonymous ? undefined : entry.npub);
const meta = data?.metadata;
const name = entry.anonymous
? '★'
: entry.name || meta?.display_name || meta?.name || entry.npub || '';
const avatar = entry.anonymous ? undefined : (entry.logo || meta?.picture);
return (
<Avatar className="h-6 w-6 ring-1 ring-white/15" title={name}>
{avatar && <AvatarImage src={avatar} alt={name} />}
<AvatarFallback className="text-[10px] font-black text-white/70">
{entry.anonymous ? '🛹' : name.replace(/[^a-zA-Z0-9]/g, '').charAt(0).toUpperCase() || 'S'}
</AvatarFallback>
</Avatar>
);
}
function TierLadder({ supporters }: { supporters: SupporterEntry[] }) {
const { t } = useTranslation();
return (
<div className="mt-5 grid grid-cols-1 gap-3 sm:grid-cols-3">
{TIER_ORDER.map((tier) => {
const meta = TIER_META[tier];
const members = supporters.filter((s) => tierOf(s.sats) === tier);
return (
<div
key={tier}
className="rounded-xl border p-3"
style={{
borderColor: `${meta.color}55`,
background: `linear-gradient(135deg, ${meta.color}14, transparent)`,
boxShadow: tier === 'legend' ? `0 0 18px ${meta.color}33` : undefined,
}}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-black uppercase tracking-wide" style={{ color: meta.color }}>
{meta.emoji} {t(meta.key)}
</span>
<span className="text-[10px] text-gray-500">{TIER_RANGE[tier]}</span>
</div>
{members.length > 0 ? (
<div className="mt-2 flex flex-wrap items-center gap-1">
{members.slice(0, 10).map((m) => (
<MemberAvatar key={m.key} entry={m} />
))}
{members.length > 10 && (
<span className="text-[10px] text-gray-500">+{members.length - 10}</span>
)}
</div>
) : (
<p className="mt-2 text-[10px] text-gray-600">{t('walletUi.supporters.tierEmpty')}</p>
)}
</div>
);
})}
</div>
);
}
function SupporterRow({ rank, entry }: { rank: number; entry: SupporterEntry }) {
const { t } = useTranslation();
const { data } = useAuthor(entry.anonymous ? undefined : entry.npub);
@ -262,7 +328,7 @@ function SupporterRow({ rank, entry }: { rank: number; entry: SupporterEntry })
: entry.name || meta?.display_name || meta?.name
|| (entry.npub ? `${entry.npub.slice(0, 12)}…${entry.npub.slice(-6)}` : t('walletUi.supporters.anonymousBacker'));
const avatar = entry.anonymous ? undefined : (entry.logo || meta?.picture);
const tier = TIER_META[tierOf(entry.angorSats || undefined)];
const tier = TIER_META[tierOf(entry.sats)];
const profileHref = !entry.anonymous && entry.npub ? `/${entry.npub}` : undefined;
const linkHref = profileHref || entry.url;
const proofUrl = entry.txid
@ -301,14 +367,17 @@ function SupporterRow({ rank, entry }: { rank: number; entry: SupporterEntry })
{entry.zap && <Zap className="h-3 w-3 shrink-0 text-[#f7931a]" />}
</div>
<div className="mt-0.5 flex items-center gap-2">
{entry.angor && (
<span
className="rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide"
style={{ background: `${tier.color}22`, color: tier.color }}
>
{t(tier.key)}
</span>
)}
<span
className="rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide"
style={{
background: `${tier.color}22`,
color: tier.color,
border: `1px solid ${tier.color}55`,
boxShadow: entry.sats >= 1_000_000 ? `0 0 10px ${tier.color}66` : undefined,
}}
>
{tier.emoji} {t(tier.key)}
</span>
{entry.note && !entry.anonymous && <span className="text-[11px] text-gray-500">{entry.note}</span>}
</div>
</div>
@ -589,9 +658,7 @@ export default function SupportersPage() {
<p className="mt-2 text-center text-xs text-gray-600">
{t('walletUi.supporters.quietNote')}
</p>
<p className="mt-3 text-center text-[11px] text-gray-600">
{t('walletUi.supporters.tiersNote')}
</p>
<TierLadder supporters={supporters} />
</>
) : (
<div className="rounded-xl border border-dashed border-[#22d3ee]/20 p-8 text-center">