Files
grimoire/src/lib/nip75-helpers.ts
Alejandro 13fec0345f feat: zap goals nip-75 (#174)
* feat: add NIP-75 Zap Goal rendering

Add feed and detail view rendering for kind 9041 (Zap Goals):

- GoalRenderer: Shows clickable title, description, and progress bar
  with target/raised amounts
- GoalDetailRenderer: Adds sorted contributor breakdown with
  individual contribution totals
- nip75-helpers: Helper functions for extracting goal metadata
  (amount, relays, deadline, beneficiaries)

Both views fetch and tally zaps from the goal's specified relays.

* fix: improve NIP-75 goal rendering

- Remove icons from goal renderers
- Remove "sats" suffixes from amounts
- Use muted text instead of destructive color for closed goals
- Content is the title, summary tag is the description
- Only show description if summary tag exists

* feat: add zap button to goal renderers

Show a 'Zap this Goal' button in both feed and detail views
when the goal is still open (not past its closed_at deadline).

* style: unify progress indicator styles

- Update user menu and welcome page progress indicators to match
  goal renderer style: bar on top, progress/total below with percentage
- Remove "sats" suffix from progress displays
- Make goal zap button primary variant and full-width

* fix: polish goal renderers for release

- Remove limit parameter from useTimeline (use whatever relays send)
- Add more spacing between "Support Grimoire" header and progress bar

* refactor: extract useGoalProgress hook for NIP-75 goals

- Create useGoalProgress hook with shared goal logic
- Handle relay selection: goal relays → user inbox → aggregators
- Calculate progress, contributors, and all metadata in one place
- Simplify both GoalRenderer and GoalDetailRenderer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 17:28:57 +01:00

117 lines
3.2 KiB
TypeScript

import type { NostrEvent } from "@/types/nostr";
import { getTagValue } from "applesauce-core/helpers";
/**
* NIP-75 Helper Functions
* Utility functions for parsing NIP-75 Zap Goal events (kind 9041)
*/
/**
* Get the target amount for a goal in millisatoshis
* @param event Goal event (kind 9041)
* @returns Target amount in millisats or undefined
*/
export function getGoalAmount(event: NostrEvent): number | undefined {
const amount = getTagValue(event, "amount");
if (!amount) return undefined;
const parsed = parseInt(amount, 10);
return isNaN(parsed) ? undefined : parsed;
}
/**
* Get the relays where zaps should be sent and tallied
* @param event Goal event (kind 9041)
* @returns Array of relay URLs
*/
export function getGoalRelays(event: NostrEvent): string[] {
const relaysTag = event.tags.find((t) => t[0] === "relays");
if (!relaysTag) return [];
const [, ...relays] = relaysTag;
return relays.filter(Boolean);
}
/**
* Get the deadline timestamp after which zaps should not be counted
* @param event Goal event (kind 9041)
* @returns Unix timestamp in seconds or undefined
*/
export function getGoalClosedAt(event: NostrEvent): number | undefined {
const closedAt = getTagValue(event, "closed_at");
if (!closedAt) return undefined;
const parsed = parseInt(closedAt, 10);
return isNaN(parsed) ? undefined : parsed;
}
/**
* Get the summary/brief description of the goal
* @param event Goal event (kind 9041)
* @returns Summary string or undefined
*/
export function getGoalSummary(event: NostrEvent): string | undefined {
return getTagValue(event, "summary");
}
/**
* Get the image URL for the goal
* @param event Goal event (kind 9041)
* @returns Image URL or undefined
*/
export function getGoalImage(event: NostrEvent): string | undefined {
return getTagValue(event, "image");
}
/**
* Get the external URL linked to the goal
* @param event Goal event (kind 9041)
* @returns URL string or undefined
*/
export function getGoalUrl(event: NostrEvent): string | undefined {
return getTagValue(event, "r");
}
/**
* Get the addressable event pointer linked to the goal
* @param event Goal event (kind 9041)
* @returns Address pointer string (kind:pubkey:identifier) or undefined
*/
export function getGoalLinkedAddress(event: NostrEvent): string | undefined {
return getTagValue(event, "a");
}
/**
* Get all beneficiary pubkeys from zap tags
* @param event Goal event (kind 9041)
* @returns Array of beneficiary pubkeys
*/
export function getGoalBeneficiaries(event: NostrEvent): string[] {
return event.tags
.filter((t) => t[0] === "zap")
.map((t) => t[1])
.filter(Boolean);
}
/**
* Check if a goal has closed (deadline passed)
* @param event Goal event (kind 9041)
* @returns true if goal is closed, false otherwise
*/
export function isGoalClosed(event: NostrEvent): boolean {
const closedAt = getGoalClosedAt(event);
if (!closedAt) return false;
return Date.now() / 1000 > closedAt;
}
/**
* Get a display title for the goal
* Content is the goal title per NIP-75
* @param event Goal event (kind 9041)
* @returns Display title string
*/
export function getGoalTitle(event: NostrEvent): string {
const content = event.content?.trim();
if (content) {
return content;
}
return "Untitled Goal";
}