feat(supporters): top 21, extravagant auto tiers + tier ladder with supporter avatars
This commit is contained in:
parent
66d83123cd
commit
cc1883cf82
3 changed files with 85 additions and 16 deletions
|
|
@ -6501,6 +6501,7 @@ walletUi: {
|
||||||
supportersTotals: '{{count}} supporters · {{angor}} sats via Angor · {{zap}} sats zapped',
|
supportersTotals: '{{count}} supporters · {{angor}} sats via Angor · {{zap}} sats zapped',
|
||||||
quietNote: 'Plus everyone who gave quietly — thank you. ❤️',
|
quietNote: 'Plus everyone who gave quietly — thank you. ❤️',
|
||||||
tiersNote: 'Tiers: Supporter under 100k sats · Patron 100k–1M · Legend 1M+',
|
tiersNote: 'Tiers: Supporter under 100k sats · Patron 100k–1M · Legend 1M+',
|
||||||
|
tierEmpty: 'No one yet — be the first.',
|
||||||
joinSupporters: 'Join {{count}} supporters',
|
joinSupporters: 'Join {{count}} supporters',
|
||||||
showAll: 'Show all {{count}}',
|
showAll: 'Show all {{count}}',
|
||||||
showLess: 'Show less',
|
showLess: 'Show less',
|
||||||
|
|
|
||||||
|
|
@ -6501,6 +6501,7 @@ walletUi: {
|
||||||
supportersTotals: '{{count}} patrocinadores · {{angor}} sats vía Angor · {{zap}} sats zapeados',
|
supportersTotals: '{{count}} patrocinadores · {{angor}} sats vía Angor · {{zap}} sats zapeados',
|
||||||
quietNote: 'Y a todos los que aportaron en silencio — gracias. ❤️',
|
quietNote: 'Y a todos los que aportaron en silencio — gracias. ❤️',
|
||||||
tiersNote: 'Niveles: Colaborador menos de 100k sats · Patrón 100k–1M · Leyenda 1M+',
|
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',
|
joinSupporters: 'Únete a {{count}} patrocinadores',
|
||||||
showAll: 'Ver los {{count}}',
|
showAll: 'Ver los {{count}}',
|
||||||
showLess: 'Ver menos',
|
showLess: 'Ver menos',
|
||||||
|
|
|
||||||
|
|
@ -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 —
|
// platform zap is already ≥ 2026-01, so this doesn't currently trim the list —
|
||||||
// the visible cap below is what keeps it short.
|
// the visible cap below is what keeps it short.
|
||||||
const SUPPORTERS_SINCE = Math.floor(Date.UTC(2025, 11, 1) / 1000); // 2025-12-01
|
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 ──────────────────
|
// ─── Static data — edit these arrays to add/remove entries ──────────────────
|
||||||
|
|
||||||
|
|
@ -248,11 +248,77 @@ interface SupporterEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIER_META = {
|
const TIER_META = {
|
||||||
supporter: { color: '#22d3ee', key: 'walletUi.supporters.tierSupporter' },
|
supporter: { color: '#22d3ee', key: 'walletUi.supporters.tierSupporter', emoji: '🛹' },
|
||||||
patron: { color: '#f7931a', key: 'walletUi.supporters.tierPatron' },
|
patron: { color: '#f7931a', key: 'walletUi.supporters.tierPatron', emoji: '🔥' },
|
||||||
legend: { color: '#facc15', key: 'walletUi.supporters.tierLegend' },
|
legend: { color: '#facc15', key: 'walletUi.supporters.tierLegend', emoji: '👑💎✨' },
|
||||||
} as const;
|
} 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 }) {
|
function SupporterRow({ rank, entry }: { rank: number; entry: SupporterEntry }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data } = useAuthor(entry.anonymous ? undefined : entry.npub);
|
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.name || meta?.display_name || meta?.name
|
||||||
|| (entry.npub ? `${entry.npub.slice(0, 12)}…${entry.npub.slice(-6)}` : t('walletUi.supporters.anonymousBacker'));
|
|| (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 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 profileHref = !entry.anonymous && entry.npub ? `/${entry.npub}` : undefined;
|
||||||
const linkHref = profileHref || entry.url;
|
const linkHref = profileHref || entry.url;
|
||||||
const proofUrl = entry.txid
|
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]" />}
|
{entry.zap && <Zap className="h-3 w-3 shrink-0 text-[#f7931a]" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 flex items-center gap-2">
|
<div className="mt-0.5 flex items-center gap-2">
|
||||||
{entry.angor && (
|
<span
|
||||||
<span
|
className="rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide"
|
||||||
className="rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide"
|
style={{
|
||||||
style={{ background: `${tier.color}22`, color: tier.color }}
|
background: `${tier.color}22`,
|
||||||
>
|
color: tier.color,
|
||||||
{t(tier.key)}
|
border: `1px solid ${tier.color}55`,
|
||||||
</span>
|
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>}
|
{entry.note && !entry.anonymous && <span className="text-[11px] text-gray-500">{entry.note}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -589,9 +658,7 @@ export default function SupportersPage() {
|
||||||
<p className="mt-2 text-center text-xs text-gray-600">
|
<p className="mt-2 text-center text-xs text-gray-600">
|
||||||
{t('walletUi.supporters.quietNote')}
|
{t('walletUi.supporters.quietNote')}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-center text-[11px] text-gray-600">
|
<TierLadder supporters={supporters} />
|
||||||
{t('walletUi.supporters.tiersNote')}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-xl border border-dashed border-[#22d3ee]/20 p-8 text-center">
|
<div className="rounded-xl border border-dashed border-[#22d3ee]/20 p-8 text-center">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue