+ {/* Header */}
+
+
{title}
+
+ {/* Description (summary tag only) */}
+ {summary && (
+
{summary}
+ )}
+
+ {/* Deadline */}
+ {closedAt && (
+
+ {closed ? (
+ Closed on {deadlineText}
+ ) : (
+ Ends {deadlineText}
+ )}
+
+ )}
+
+
+ {/* Progress Section */}
+ {targetSats > 0 && (
+
+
+
+ {raisedSats.toLocaleString()}
+
+
+ of {targetSats.toLocaleString()}
+
+
+
+
+ {contributors.length} contributors
+
+ {progress.toFixed(1)}%
+
+
+
+ )}
+
+ {/* Zap Button */}
+ {!closed && (
+
+ )}
+
+ {/* Beneficiaries */}
+ {beneficiaries.length > 0 && (
+
+
+ Beneficiaries
+
+
+ {beneficiaries.map((pubkey) => (
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Contributors */}
+
+
+ Contributors
+
+
+ {loading && contributors.length === 0 ? (
+
+ {[...Array(5)].map((_, i) => (
+
+
+
+
+ ))}
+
+ ) : contributors.length === 0 ? (
+
+ No contributions yet. Be the first to contribute!
+
+ ) : (
+
+ {contributors.map((contributor, index) => {
+ const amountSats = Math.floor(contributor.totalAmount / 1000);
+ return (
+
+
+
+ #{index + 1}
+
+
+ {contributor.zapCount > 1 && (
+
+ ({contributor.zapCount} zaps)
+
+ )}
+
+
+ {amountSats.toLocaleString()}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/nostr/kinds/GoalRenderer.tsx b/src/components/nostr/kinds/GoalRenderer.tsx
new file mode 100644
index 0000000..508d1fe
--- /dev/null
+++ b/src/components/nostr/kinds/GoalRenderer.tsx
@@ -0,0 +1,106 @@
+import {
+ BaseEventProps,
+ BaseEventContainer,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import { Zap } from "lucide-react";
+import { useGoalProgress } from "@/hooks/useGoalProgress";
+import { formatTimestamp } from "@/hooks/useLocale";
+import { useGrimoire } from "@/core/state";
+import { Progress } from "@/components/ui/progress";
+import { Button } from "@/components/ui/button";
+
+/**
+ * Renderer for Kind 9041 - Zap Goals (NIP-75)
+ * Shows goal title, description, and funding progress
+ */
+export function GoalRenderer({ event }: BaseEventProps) {
+ const { locale, addWindow } = useGrimoire();
+ const {
+ title,
+ summary,
+ closedAt,
+ closed,
+ targetSats,
+ raisedSats,
+ progress,
+ loading,
+ zaps,
+ } = useGoalProgress(event);
+
+ const deadlineText = closedAt
+ ? formatTimestamp(closedAt, "absolute", locale.locale)
+ : null;
+
+ const handleZap = () => {
+ addWindow("zap", {
+ recipientPubkey: event.pubkey,
+ eventPointer: { id: event.id },
+ });
+ };
+
+ return (
+