diff --git a/src/components/ZapCrewDialog.tsx b/src/components/ZapCrewDialog.tsx new file mode 100644 index 0000000..2e66f1f --- /dev/null +++ b/src/components/ZapCrewDialog.tsx @@ -0,0 +1,218 @@ +import { useState, useMemo } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAuthor } from '@/hooks/useAuthor'; +import { useWallet } from '@/hooks/useWallet'; +import { useCrewZap, splitAmount, type CrewZapResult } from '@/hooks/useCrewZap'; +import { useToast } from '@/hooks/useToast'; +import { genUserName } from '@/lib/genUserName'; +import { Zap, Check, Copy, Loader2, X, Users, AlertCircle } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const PRESETS = [210, 500, 1000, 2100, 5000]; + +interface ZapCrewDialogProps { + crewName: string; + /** Recipient pubkeys β€” typically founder + confirmed members. */ + recipients: string[]; + children: React.ReactNode; +} + +function RecipientRow({ result }: { result: CrewZapResult }) { + const author = useAuthor(result.pubkey); + const meta = author.data?.metadata; + const name = meta?.name ?? genUserName(result.pubkey); + const { toast } = useToast(); + + const copyInvoice = async () => { + if (!result.invoice) return; + await navigator.clipboard.writeText(result.invoice); + toast({ title: 'Invoice copied', description: 'Paste it into any Lightning wallet' }); + }; + + return ( +
+ + + {name.slice(0, 2).toUpperCase()} + +
+

{name}

+

{result.amount.toLocaleString()} sats

+
+ +
+ ); +} + +function StatusPill({ result, onCopy }: { result: CrewZapResult; onCopy: () => void }) { + switch (result.status) { + case 'success': + return Sent; + case 'resolving': + case 'paying': + return {result.status === 'paying' ? 'Paying' : 'Resolving'}; + case 'skipped': + return Skipped; + case 'failed': + return result.invoice ? ( + + ) : ( + Failed + ); + default: + return Waiting; + } +} + +export function ZapCrewDialog({ crewName, recipients, children }: ZapCrewDialogProps) { + const [open, setOpen] = useState(false); + const { user } = useCurrentUser(); + const { preferredMethod } = useWallet(); + const { zapCrew, results, isRunning, reset } = useCrewZap(); + const [amount, setAmount] = useState(1000); + const [comment, setComment] = useState(''); + + // Don't include the current user as a recipient (no zapping yourself). + const payable = useMemo( + () => recipients.filter((pk) => pk !== user?.pubkey), + [recipients, user?.pubkey] + ); + + const numAmount = typeof amount === 'string' ? parseInt(amount, 10) || 0 : amount; + const perHead = payable.length > 0 ? splitAmount(numAmount, payable.length) : []; + const started = results.length > 0; + const doneCount = results.filter((r) => r.status === 'success').length; + const allDone = started && !isRunning; + + const handleZap = () => { + if (numAmount <= 0 || payable.length === 0) return; + zapCrew(payable, numAmount, comment || `Zapped the ${crewName} crew! 🀘`); + }; + + const handleOpenChange = (o: boolean) => { + setOpen(o); + if (!o) { + reset(); + setAmount(1000); + setComment(''); + } + }; + + if (!user) return <>{children}; + + return ( + + +
{children}
+
+ + + + Zap the Crew + + + Split a zap evenly across {payable.length} crew {payable.length === 1 ? 'member' : 'members'} with a Lightning address. + + + + {payable.length === 0 ? ( +
+ No other crew members to zap right now. +
+ ) : !started ? ( +
+
+ + v && setAmount(parseInt(v, 10))} + className="grid grid-cols-5 gap-1 mt-2" + > + {PRESETS.map((p) => ( + + + {p >= 1000 ? `${p / 1000}k` : p} + + ))} + + setAmount(e.target.value)} + placeholder="Custom total" + className="mt-2 bg-gray-800 border-gray-700" + /> +
+ + {numAmount > 0 && ( +

+ β‰ˆ {perHead[0]?.toLocaleString()} sats each + {perHead.length > 1 && perHead[0] !== perHead[perHead.length - 1] && ' (rounded)'} +

+ )} + +