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 { 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">
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue