diff --git a/src/modules/activities/components/ActivityCard.vue b/src/modules/activities/components/ActivityCard.vue
index d021b10..b93d20a 100644
--- a/src/modules/activities/components/ActivityCard.vue
+++ b/src/modules/activities/components/ActivityCard.vue
@@ -155,13 +155,18 @@ const placeholderBg = computed(() => {
{{ activity.location }}
-
+
-
+
+ {{ t('activities.detail.unlimitedTickets', 'Unlimited tickets') }}
+
+
{{ t('activities.detail.ticketsAvailable', { count: activity.ticketInfo.available }) }}
diff --git a/src/modules/activities/types/activity.ts b/src/modules/activities/types/activity.ts
index a377e8b..993afa7 100644
--- a/src/modules/activities/types/activity.ts
+++ b/src/modules/activities/types/activity.ts
@@ -1,6 +1,6 @@
import ngeohash from 'ngeohash'
import type { ActivityCategory } from './category'
-import type { CalendarTimeEvent, CalendarDateEvent } from './nip52'
+import type { CalendarTimeEvent, CalendarDateEvent, TicketTags } from './nip52'
import type { TicketedEvent } from './ticket'
/**
@@ -74,8 +74,26 @@ export interface OrganizerInfo {
export interface ActivityTicketInfo {
price: number
currency: string
- available: number
- total: number
+ /** Remaining capacity. Undefined means unlimited. */
+ available?: number
+ /** Running paid count. */
+ sold: number
+ /** Whether the organizer enabled fiat checkout. */
+ allowFiat: boolean
+ /** Fiat settle currency when allowFiat is true. */
+ fiatCurrency?: string
+}
+
+function ticketTagsToInfo(ticket: TicketTags | undefined): ActivityTicketInfo | undefined {
+ if (!ticket) return undefined
+ return {
+ price: ticket.price,
+ currency: ticket.currency,
+ available: ticket.available,
+ sold: ticket.sold,
+ allowFiat: ticket.allowFiat,
+ fiatCurrency: ticket.fiatCurrency,
+ }
}
/**
@@ -104,6 +122,7 @@ export function calendarTimeEventToActivity(event: CalendarTimeEvent, organizer?
geohash: event.geohash,
category,
tags: event.hashtags,
+ ticketInfo: ticketTagsToInfo(event.ticket),
isPrivate: false,
createdAt: new Date(event.createdAt * 1000),
}
@@ -140,6 +159,7 @@ export function calendarDateEventToActivity(event: CalendarDateEvent, organizer?
geohash: event.geohash,
category,
tags: event.hashtags,
+ ticketInfo: ticketTagsToInfo(event.ticket),
isPrivate: false,
createdAt: new Date(event.createdAt * 1000),
}
diff --git a/src/modules/activities/types/nip52.ts b/src/modules/activities/types/nip52.ts
index 332d054..b87db35 100644
--- a/src/modules/activities/types/nip52.ts
+++ b/src/modules/activities/types/nip52.ts
@@ -17,6 +17,27 @@ export const NIP52_KINDS = {
export type Nip52Kind = typeof NIP52_KINDS[keyof typeof NIP52_KINDS]
+/**
+ * AIO custom tags carried on ticketed calendar events. The aiolabs/events
+ * extension adds these so connected clients can render the buy CTA + the
+ * "X tickets remaining" badge without an extra REST hop. Absent when the
+ * event was published by a non-AIO client.
+ */
+export interface TicketTags {
+ /** Remaining capacity. Undefined means unlimited. */
+ available?: number
+ /** Running paid-count. */
+ sold: number
+ /** Price per ticket in the event's `currency`. */
+ price: number
+ /** Currency string (e.g. 'sat', 'sats', 'USD'). */
+ currency: string
+ /** Whether the organizer enabled fiat checkout. */
+ allowFiat: boolean
+ /** Fiat settle currency when allowFiat is true. */
+ fiatCurrency?: string
+}
+
/**
* Parsed NIP-52 date-based calendar event (kind 31922)
*/
@@ -36,6 +57,7 @@ export interface CalendarDateEvent {
references: string[]
id: string
createdAt: number
+ ticket?: TicketTags
}
/**
@@ -59,6 +81,7 @@ export interface CalendarTimeEvent {
references: string[]
id: string
createdAt: number
+ ticket?: TicketTags
}
export interface Participant {
@@ -96,6 +119,35 @@ function getTagValues(tags: string[][], tagName: string): string[] {
return tags.filter(t => t[0] === tagName).map(t => t[1])
}
+/**
+ * Parse the AIO ticket_* tags off a NIP-52 calendar event. Returns
+ * undefined when the event carries no ticket info (e.g. an event
+ * published by a non-AIO client or a non-ticketed AIO event — though
+ * the latter doesn't currently exist since every aiolabs/events row
+ * has a price + currency).
+ *
+ * `tickets_currency` is the discriminator: when absent, the event has
+ * no inventory metadata and the buy UI stays hidden.
+ */
+function parseTicketTags(tags: string[][]): TicketTags | undefined {
+ const currency = getTagValue(tags, 'tickets_currency')
+ if (!currency) return undefined
+
+ const availableStr = getTagValue(tags, 'tickets_available')
+ const soldStr = getTagValue(tags, 'tickets_sold')
+ const priceStr = getTagValue(tags, 'tickets_price')
+ const allowFiatStr = getTagValue(tags, 'tickets_allow_fiat')
+
+ return {
+ available: availableStr != null ? Number(availableStr) : undefined,
+ sold: soldStr != null ? Number(soldStr) : 0,
+ price: priceStr != null ? Number(priceStr) : 0,
+ currency,
+ allowFiat: allowFiatStr === 'true',
+ fiatCurrency: getTagValue(tags, 'tickets_fiat_currency'),
+ }
+}
+
/**
* Parse a NIP-52 start/end tag value to a unix timestamp in seconds.
* Handles: unix seconds, unix milliseconds, and ISO date strings.
@@ -166,6 +218,7 @@ export function parseCalendarTimeEvent(event: NostrEvent): CalendarTimeEvent | n
references: getTagValues(event.tags, 'r'),
id: event.id,
createdAt: event.created_at,
+ ticket: parseTicketTags(event.tags),
}
}
@@ -213,6 +266,7 @@ export function parseCalendarDateEvent(event: NostrEvent): CalendarDateEvent | n
references: getTagValues(event.tags, 'r'),
id: event.id,
createdAt: event.created_at,
+ ticket: parseTicketTags(event.tags),
}
}