SkateSpots/src/components/FundingGoalCard.tsx
SkateSpots b6c33c8a9b feat(events): crowdfunding funding goals via zaps
CreateEventPage gains an optional 'Funding Goal (sats)' field that publishes a
'goal' tag. EventDetailPage shows a FundingGoalCard with live raised/target
progress, backer count and a Fund button — funded peer-to-peer via NIP-57 zaps
(no escrow).
2026-09-26 17:24:42 +07:00

79 lines
2.9 KiB
TypeScript

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ZapDialog } from '@/components/ZapDialog';
import { useZaps } from '@/hooks/useZaps';
import { useWallet } from '@/hooks/useWallet';
import { cn } from '@/lib/utils';
import { Zap } from 'lucide-react';
import type { Event } from 'nostr-tools';
/**
* Crowdfunding progress for an event that declares a `goal` tag (sats).
* "Raised" is the live total of NIP-57 zaps to the event, so backers fund it
* peer-to-peer — no escrow, no platform in the middle.
*/
export function FundingGoalCard({
event,
goalSats,
className,
}: {
event: Event;
goalSats: number;
className?: string;
}) {
const { webln, activeNWC } = useWallet();
const { totalSats, zapCount, isLoading } = useZaps(event, webln, activeNWC);
const pct = goalSats > 0 ? Math.min(100, Math.round((totalSats / goalSats) * 100)) : 0;
const remaining = Math.max(0, goalSats - totalSats);
const reached = remaining === 0;
return (
<Card className={cn('border-2 border-[#ccff00]/40 bg-black/90', className)}>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg text-[#ccff00]">
<Zap className="h-5 w-5" />
Funding Goal
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-baseline justify-between gap-2">
<span className="text-2xl font-black text-white">
{isLoading ? '…' : totalSats.toLocaleString()}
<span className="ml-1 text-sm font-bold text-gray-400">
/ {goalSats.toLocaleString()} sats
</span>
</span>
<span className="text-sm font-bold text-[#ccff00]">{pct}%</span>
</div>
<div className="h-3 w-full overflow-hidden rounded-full bg-white/10">
<div
className="h-full rounded-full bg-gradient-to-r from-[#ccff00] to-[#00ff88] transition-all duration-500"
style={{ width: `${pct}%` }}
/>
</div>
<p className="text-xs text-gray-400">
{reached ? (
<>🎉 Goal reached! Funded by {zapCount.toLocaleString()} backer{zapCount === 1 ? '' : 's'}.</>
) : (
<>
<span className="font-bold text-white">{remaining.toLocaleString()}</span> sats to go ·{' '}
{zapCount.toLocaleString()} backer{zapCount === 1 ? '' : 's'}
</>
)}
</p>
<ZapDialog target={event}>
<button
type="button"
className="flex w-full items-center justify-center gap-2 rounded-md bg-[#ccff00] py-2 text-sm font-bold uppercase tracking-wide text-black transition-colors hover:bg-[#ccff00]/90"
>
<Zap className="h-4 w-4" />
{reached ? 'Keep supporting' : 'Fund this event'}
</button>
</ZapDialog>
</CardContent>
</Card>
);
}