mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-14 01:16:58 +02:00
* 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>
117 lines
3.2 KiB
TypeScript
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";
|
|
}
|