diff --git a/src/components/FundingGoalCard.tsx b/src/components/FundingGoalCard.tsx new file mode 100644 index 0000000..e62c73c --- /dev/null +++ b/src/components/FundingGoalCard.tsx @@ -0,0 +1,79 @@ +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 ( + + + + + Funding Goal + + + +
+ + {isLoading ? 'โ€ฆ' : totalSats.toLocaleString()} + + / {goalSats.toLocaleString()} sats + + + {pct}% +
+ +
+
+
+ +

+ {reached ? ( + <>๐ŸŽ‰ Goal reached! Funded by {zapCount.toLocaleString()} backer{zapCount === 1 ? '' : 's'}. + ) : ( + <> + {remaining.toLocaleString()} sats to go ยท{' '} + {zapCount.toLocaleString()} backer{zapCount === 1 ? '' : 's'} + + )} +

+ + + + + + + ); +} diff --git a/src/pages/CreateEventPage.tsx b/src/pages/CreateEventPage.tsx index 4e7ae1d..b3f89c4 100644 --- a/src/pages/CreateEventPage.tsx +++ b/src/pages/CreateEventPage.tsx @@ -112,6 +112,8 @@ export default function CreateEventPage() { // Paid event / entry fee const [entryFee, setEntryFee] = useState(''); const isPaidEvent = entryFee.trim().length > 0 && entryFee.trim() !== '0'; + // Optional crowdfunding goal in sats (NIP-57 zaps fund the event). + const [fundingGoal, setFundingGoal] = useState(''); // Optional sponsors (logo strip on the event page). const [sponsors, setSponsors] = useState([]); @@ -366,6 +368,12 @@ export default function CreateEventPage() { tags.push(['t', 'paid']); } + // Optional crowdfunding goal (sats) โ€” shown as a funding progress bar and + // funded peer-to-peer via zaps (no escrow). + if (fundingGoal.trim() && Number(fundingGoal) > 0) { + tags.push(['goal', String(Math.floor(Number(fundingGoal)))]); + } + // Featured listing (optional, paid) if (isFeatured && featuredUntilSeconds) { tags.push(['featured', String(featuredUntilSeconds)]); @@ -1016,6 +1024,25 @@ export default function CreateEventPage() { />
+ {/* Funding goal (optional) โ€” community crowdfunding via zaps */} +
+ + setFundingGoal(e.target.value)} + className="bg-black/50 border-white/20" + /> +
+ {/* Sponsors (optional) */}
diff --git a/src/pages/EventDetailPage.tsx b/src/pages/EventDetailPage.tsx index 6bef82f..e06490a 100644 --- a/src/pages/EventDetailPage.tsx +++ b/src/pages/EventDetailPage.tsx @@ -19,6 +19,7 @@ import { EventSponsors } from '@/components/EventSponsors'; import { CommentsSection } from '@/components/comments/CommentsSection'; import { DeleteEntityDialog } from '@/components/DeleteEntityDialog'; import { useSkateEvent } from '@/hooks/useSkateEvents'; +import { FundingGoalCard } from '@/components/FundingGoalCard'; import { useEventRsvps, eventCoord } from '@/hooks/useEventRsvps'; import { useEventUpdates } from '@/hooks/useEventUpdates'; import { useAuthor } from '@/hooks/useAuthor'; @@ -147,6 +148,9 @@ export default function EventDetailPage() { // NIP-52 RSVPs for this event (counts + attendee avatars). const rsvpSummary = useEventRsvps(event ? eventCoord(event) : undefined); + // Optional crowdfunding goal (sats) set by the host as a `goal` tag. + const goalSats = Number(event?.event.tags.find(([t]) => t === 'goal')?.[1] ?? 0) || 0; + // Host updates โ†’ detect if the host cancelled or rescheduled, so we can warn // attendees loudly instead of letting them show up to a dead jam. const { data: eventUpdates } = useEventUpdates(event ? eventCoord(event) : undefined); @@ -532,6 +536,11 @@ export default function EventDetailPage() { + {/* Crowdfunding goal (only when the host set one) */} + {goalSats > 0 && ( + + )} + {/* RSVP / Attendance */}