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).
79 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|