feat(supporters): on-chain verify chips, real avatars/names, fiat total, footer count
This commit is contained in:
parent
918c289a71
commit
1bdaa57aa3
5 changed files with 54 additions and 15 deletions
|
|
@ -3,6 +3,7 @@ import { Link, Outlet, useLocation } from 'react-router-dom';
|
|||
import { nip19 } from 'nostr-tools';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isNativePlatform } from '@/lib/platform';
|
||||
import { BACKERS } from '@/lib/backers';
|
||||
import { useNavPreferences, useDesktopNavPreferences, type NavTab } from '@/hooks/useNavPreferences';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger, SheetTitle } from '@/components/ui/sheet';
|
||||
|
|
@ -1101,6 +1102,9 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
>
|
||||
{t('chromeUi.collaborators')}
|
||||
</Link>
|
||||
<span className="text-gray-500">
|
||||
{t('walletUi.supporters.supportedBy', { count: BACKERS.length })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span>{t('chromeUi.notSponsoredBy')}</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
|
|
|
|||
|
|
@ -6500,6 +6500,8 @@ walletUi: {
|
|||
anonymousBacker: 'Anonymous supporter',
|
||||
beFirstBacker: 'No public supporters yet — be the first.',
|
||||
backersTotal: '{{count}} supporters · {{amount}} sats raised',
|
||||
verifiedOnChain: 'On-chain verified',
|
||||
supportedBy: 'Supported by {{count}}',
|
||||
support: 'Support',
|
||||
supportDesc: 'Fund SkateSpots on Angor — decentralized, on-chain Bitcoin crowdfunding. No middleman, no KYC.',
|
||||
collaborators: 'Collaborators',
|
||||
|
|
|
|||
|
|
@ -6500,6 +6500,8 @@ walletUi: {
|
|||
anonymousBacker: 'Patrocinador anónimo',
|
||||
beFirstBacker: 'Aún no hay patrocinadores públicos — sé el primero.',
|
||||
backersTotal: '{{count}} patrocinadores · {{amount}} sats recaudados',
|
||||
verifiedOnChain: 'Verificado on-chain',
|
||||
supportedBy: 'Apoyado por {{count}}',
|
||||
support: 'Apoyo',
|
||||
supportDesc: 'Financia SkateSpots en Angor — crowdfunding descentralizado on-chain en Bitcoin. Sin intermediarios, sin KYC.',
|
||||
collaborators: 'Colaboradores',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export interface Backer {
|
|||
logo?: string;
|
||||
/** External link (used when there is no npub). */
|
||||
url?: string;
|
||||
/** Investment transaction id — renders a verifiable "on-chain" chip. */
|
||||
txid?: string;
|
||||
/** Amount contributed, in satoshis. */
|
||||
amount?: number;
|
||||
/** Small line under the name, e.g. "Founding backer". */
|
||||
|
|
@ -34,11 +36,13 @@ export interface Backer {
|
|||
export const BACKERS: Backer[] = [
|
||||
{
|
||||
npub: 'npub15q3ekfxdvzuc9nhhyh2yv7lwtnfagurllc9rkkrypt40d303jsjsdfujeu',
|
||||
txid: '4f95d18ff44a86e8302d923f53fe3777c52236ec087b0d374b8e9566995e1bcc',
|
||||
amount: 99000,
|
||||
note: 'Founding supporter',
|
||||
},
|
||||
{
|
||||
npub: 'npub1t5yjluyvxatlmmcxccv7tdppmp4ae0j850k63323t7j0fe4lsqlsp7mfyl',
|
||||
txid: '5dbf872fd8f02ecf2b534857b0613e880a021478c6e8e6483c774e95d2ddc565',
|
||||
amount: 99000,
|
||||
note: 'Founding supporter',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useSeoMeta } from '@unhead/react';
|
||||
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 { BACKERS, BACKERS_TOTAL_SATS, type Backer } from '@/lib/backers';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useSatsToUsd } from '@/hooks/useExchangeRate';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
const ANGOR_PROJECT_URL = 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z';
|
||||
|
|
@ -207,37 +209,58 @@ function FriendCard({ friend }: { friend: Friend }) {
|
|||
|
||||
function BackerCard({ backer }: { backer: Backer }) {
|
||||
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 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="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
|
||||
src={backer.logo}
|
||||
alt={backer.name || 'Backer'}
|
||||
className="w-12 h-12 object-contain"
|
||||
src={avatar}
|
||||
alt={name || 'Supporter'}
|
||||
className="w-16 h-16 object-cover"
|
||||
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>
|
||||
<p className="font-bold text-sm text-white group-hover:text-[#22d3ee] transition-colors">
|
||||
{backer.name || t('walletUi.supporters.anonymousBacker')}
|
||||
</p>
|
||||
<div className="min-w-0">
|
||||
{href && backer.npub ? (
|
||||
<Link to={href} className="font-bold text-sm text-white hover:text-[#22d3ee] transition-colors">
|
||||
{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' && (
|
||||
<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.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>
|
||||
);
|
||||
|
||||
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) ───────────────────────────────────────────
|
||||
|
|
@ -257,6 +280,7 @@ function PlaceholderCard({ label }: { label: string }) {
|
|||
|
||||
export default function SupportersPage() {
|
||||
const { t } = useTranslation();
|
||||
const totalUsd = useSatsToUsd(BACKERS_TOTAL_SATS);
|
||||
useSeoMeta({
|
||||
title: 'Supporters - SkateSpots',
|
||||
description: 'The community keeping SkateSpots alive — supporters, collaborators, partners and friends.',
|
||||
|
|
@ -347,6 +371,9 @@ export default function SupportersPage() {
|
|||
count: BACKERS.length,
|
||||
amount: BACKERS_TOTAL_SATS.toLocaleString(),
|
||||
})}
|
||||
{typeof totalUsd === 'number' && (
|
||||
<span className="text-gray-500"> · ≈ ${totalUsd.toFixed(2)}</span>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Reference in a new issue