feat(supporters): on-chain verify chips, real avatars/names, fiat total, footer count

This commit is contained in:
SkateSpots 2026-10-05 17:56:10 +07:00
parent 918c289a71
commit 1bdaa57aa3
5 changed files with 54 additions and 15 deletions

View file

@ -3,6 +3,7 @@ import { Link, Outlet, useLocation } from 'react-router-dom';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { isNativePlatform } from '@/lib/platform'; import { isNativePlatform } from '@/lib/platform';
import { BACKERS } from '@/lib/backers';
import { useNavPreferences, useDesktopNavPreferences, type NavTab } from '@/hooks/useNavPreferences'; import { useNavPreferences, useDesktopNavPreferences, type NavTab } from '@/hooks/useNavPreferences';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger, SheetTitle } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetTrigger, SheetTitle } from '@/components/ui/sheet';
@ -1101,6 +1102,9 @@ export function Layout({ children, noPadding }: LayoutProps) {
> >
{t('chromeUi.collaborators')} {t('chromeUi.collaborators')}
</Link> </Link>
<span className="text-gray-500">
{t('walletUi.supporters.supportedBy', { count: BACKERS.length })}
</span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span>{t('chromeUi.notSponsoredBy')}</span> <span>{t('chromeUi.notSponsoredBy')}</span>
<span className="inline-flex items-center gap-1.5"> <span className="inline-flex items-center gap-1.5">

View file

@ -6500,6 +6500,8 @@ walletUi: {
anonymousBacker: 'Anonymous supporter', anonymousBacker: 'Anonymous supporter',
beFirstBacker: 'No public supporters yet — be the first.', beFirstBacker: 'No public supporters yet — be the first.',
backersTotal: '{{count}} supporters · {{amount}} sats raised', backersTotal: '{{count}} supporters · {{amount}} sats raised',
verifiedOnChain: 'On-chain verified',
supportedBy: 'Supported by {{count}}',
support: 'Support', support: 'Support',
supportDesc: 'Fund SkateSpots on Angor — decentralized, on-chain Bitcoin crowdfunding. No middleman, no KYC.', supportDesc: 'Fund SkateSpots on Angor — decentralized, on-chain Bitcoin crowdfunding. No middleman, no KYC.',
collaborators: 'Collaborators', collaborators: 'Collaborators',

View file

@ -6500,6 +6500,8 @@ walletUi: {
anonymousBacker: 'Patrocinador anónimo', anonymousBacker: 'Patrocinador anónimo',
beFirstBacker: 'Aún no hay patrocinadores públicos — sé el primero.', beFirstBacker: 'Aún no hay patrocinadores públicos — sé el primero.',
backersTotal: '{{count}} patrocinadores · {{amount}} sats recaudados', backersTotal: '{{count}} patrocinadores · {{amount}} sats recaudados',
verifiedOnChain: 'Verificado on-chain',
supportedBy: 'Apoyado por {{count}}',
support: 'Apoyo', support: 'Apoyo',
supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.', supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.',
collaborators: 'Colaboradores', collaborators: 'Colaboradores',

View file

@ -25,6 +25,8 @@ export interface Backer {
logo?: string; logo?: string;
/** External link (used when there is no npub). */ /** External link (used when there is no npub). */
url?: string; url?: string;
/** Investment transaction id — renders a verifiable "on-chain" chip. */
txid?: string;
/** Amount contributed, in satoshis. */ /** Amount contributed, in satoshis. */
amount?: number; amount?: number;
/** Small line under the name, e.g. "Founding backer". */ /** Small line under the name, e.g. "Founding backer". */
@ -34,11 +36,13 @@ export interface Backer {
export const BACKERS: Backer[] = [ export const BACKERS: Backer[] = [
{ {
npub: 'npub15q3ekfxdvzuc9nhhyh2yv7lwtnfagurllc9rkkrypt40d303jsjsdfujeu', npub: 'npub15q3ekfxdvzuc9nhhyh2yv7lwtnfagurllc9rkkrypt40d303jsjsdfujeu',
txid: '4f95d18ff44a86e8302d923f53fe3777c52236ec087b0d374b8e9566995e1bcc',
amount: 99000, amount: 99000,
note: 'Founding supporter', note: 'Founding supporter',
}, },
{ {
npub: 'npub1t5yjluyvxatlmmcxccv7tdppmp4ae0j850k63323t7j0fe4lsqlsp7mfyl', npub: 'npub1t5yjluyvxatlmmcxccv7tdppmp4ae0j850k63323t7j0fe4lsqlsp7mfyl',
txid: '5dbf872fd8f02ecf2b534857b0613e880a021478c6e8e6483c774e95d2ddc565',
amount: 99000, amount: 99000,
note: 'Founding supporter', note: 'Founding supporter',
}, },

View file

@ -1,8 +1,10 @@
import { useSeoMeta } from '@unhead/react'; import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star } from 'lucide-react'; import { ExternalLink, Zap, Heart, Users, Handshake, Building2, Star, ShieldCheck } from 'lucide-react';
import { SupportSkateSpots } from '@/components/SupportSkateSpots'; import { SupportSkateSpots } from '@/components/SupportSkateSpots';
import { BACKERS, BACKERS_TOTAL_SATS, type Backer } from '@/lib/backers'; import { BACKERS, BACKERS_TOTAL_SATS, type Backer } from '@/lib/backers';
import { useAuthor } from '@/hooks/useAuthor';
import { useSatsToUsd } from '@/hooks/useExchangeRate';
import { useTranslation } from '@/contexts/language'; import { useTranslation } from '@/contexts/language';
const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z'; const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z';
@ -207,37 +209,58 @@ function FriendCard({ friend }: { friend: Friend }) {
function BackerCard({ backer }: { backer: Backer }) { function BackerCard({ backer }: { backer: Backer }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = useAuthor(backer.npub);
const meta = data?.metadata;
const name = backer.name || meta?.display_name || meta?.name;
const avatar = backer.logo || meta?.picture;
const initial = (name || 'S').trim().charAt(0).toUpperCase();
const href = backer.npub ? `/${backer.npub}` : backer.url; const href = backer.npub ? `/${backer.npub}` : backer.url;
const inner = ( return (
<div className="flex flex-col items-center gap-3 p-5 rounded-xl bg-black/60 border border-white/10 hover:border-[#22d3ee]/40 hover:bg-[#22d3ee]/5 transition-all group text-center"> <div className="flex flex-col items-center gap-3 p-5 rounded-xl bg-black/60 border border-white/10 hover:border-[#22d3ee]/40 hover:bg-[#22d3ee]/5 transition-all group text-center">
<div className="w-16 h-16 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center overflow-hidden group-hover:border-[#22d3ee]/40 transition-all"> <div className="w-16 h-16 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center overflow-hidden group-hover:border-[#22d3ee]/40 transition-all">
{backer.logo ? ( {avatar ? (
<img <img
src={backer.logo} src={avatar}
alt={backer.name || 'Backer'} alt={name || 'Supporter'}
className="w-12 h-12 object-contain" className="w-16 h-16 object-cover"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/> />
) : ( ) : (
<span className="text-2xl">🛹</span> <span className="text-2xl font-black text-[#22d3ee]">{initial}</span>
)} )}
</div> </div>
<div> <div className="min-w-0">
<p className="font-bold text-sm text-white group-hover:text-[#22d3ee] transition-colors"> {href && backer.npub ? (
{backer.name || t('walletUi.supporters.anonymousBacker')} <Link to={href} className="font-bold text-sm text-white hover:text-[#22d3ee] transition-colors">
</p> {name || t('walletUi.supporters.anonymousBacker')}
</Link>
) : href ? (
<a href={href} target="_blank" rel="noopener noreferrer" className="font-bold text-sm text-white hover:text-[#22d3ee] transition-colors">
{name || t('walletUi.supporters.anonymousBacker')}
</a>
) : (
<p className="font-bold text-sm text-white">{name || t('walletUi.supporters.anonymousBacker')}</p>
)}
{typeof backer.amount === 'number' && ( {typeof backer.amount === 'number' && (
<p className="text-xs font-mono mt-0.5 text-[#22d3ee]/80">{backer.amount.toLocaleString()} sats</p> <p className="text-xs font-mono mt-0.5 text-[#22d3ee]/80">{backer.amount.toLocaleString()} sats</p>
)} )}
{backer.note && <p className="text-xs text-gray-500 mt-0.5">{backer.note}</p>} {backer.note && <p className="text-xs text-gray-500 mt-0.5">{backer.note}</p>}
{backer.txid && (
<a
href={`https://mempool.space/tx/${backer.txid}`}
target="_blank"
rel="noopener noreferrer"
title={backer.txid}
className="mt-1 inline-flex items-center gap-1 text-[10px] font-bold uppercase tracking-wide text-emerald-400/80 hover:text-emerald-300 transition-colors"
>
<ShieldCheck className="h-3 w-3" />
{t('walletUi.supporters.verifiedOnChain')}
</a>
)}
</div> </div>
</div> </div>
); );
if (!href) return inner;
if (backer.npub) return <Link to={`/${backer.npub}`}>{inner}</Link>;
return <a href={href} target="_blank" rel="noopener noreferrer">{inner}</a>;
} }
// ─── Placeholder card (empty slot) ─────────────────────────────────────────── // ─── Placeholder card (empty slot) ───────────────────────────────────────────
@ -257,6 +280,7 @@ function PlaceholderCard({ label }: { label: string }) {
export default function SupportersPage() { export default function SupportersPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const totalUsd = useSatsToUsd(BACKERS_TOTAL_SATS);
useSeoMeta({ useSeoMeta({
title: 'Supporters - SkateSpots', title: 'Supporters - SkateSpots',
description: 'The community keeping SkateSpots alive — supporters, collaborators, partners and friends.', description: 'The community keeping SkateSpots alive — supporters, collaborators, partners and friends.',
@ -347,6 +371,9 @@ export default function SupportersPage() {
count: BACKERS.length, count: BACKERS.length,
amount: BACKERS_TOTAL_SATS.toLocaleString(), amount: BACKERS_TOTAL_SATS.toLocaleString(),
})} })}
{typeof totalUsd === 'number' && (
<span className="text-gray-500"> · ≈ ${totalUsd.toFixed(2)}</span>
)}
</p> </p>
</> </>
) : ( ) : (