diff --git a/src/modules/activities/components/ActivityCard.vue b/src/modules/activities/components/ActivityCard.vue index d16c376..d021b10 100644 --- a/src/modules/activities/components/ActivityCard.vue +++ b/src/modules/activities/components/ActivityCard.vue @@ -4,10 +4,9 @@ import { useI18n } from 'vue-i18n' import { format } from 'date-fns' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { MapPin, Calendar, Ticket, User, CheckCircle2 } from 'lucide-vue-next' +import { MapPin, Calendar, Ticket, User } from 'lucide-vue-next' import BookmarkButton from './BookmarkButton.vue' import { useDateLocale } from '../composables/useDateLocale' -import { useOwnedTickets } from '../composables/useOwnedTickets' import type { Activity } from '../types/activity' const props = defineProps<{ @@ -20,9 +19,6 @@ const emit = defineEmits<{ const { t } = useI18n() const { dateLocale } = useDateLocale() -const { paidCount } = useOwnedTickets() - -const ownedCount = computed(() => paidCount(props.activity.id)) const dateDisplay = computed(() => { const a = props.activity @@ -159,38 +155,19 @@ const placeholderBg = computed(() => { {{ activity.location }} - +
- - {{ t('activities.detail.unlimitedTickets', 'Unlimited tickets') }} - - + {{ t('activities.detail.ticketsAvailable', { count: activity.ticketInfo.available }) }} {{ t('activities.detail.soldOut') }}
- - -
- - - {{ t('activities.detail.ticketsOwned', { count: ownedCount }, ownedCount) }} - -
diff --git a/src/modules/activities/composables/useActivities.ts b/src/modules/activities/composables/useActivities.ts index 67b49b5..78b1bb5 100644 --- a/src/modules/activities/composables/useActivities.ts +++ b/src/modules/activities/composables/useActivities.ts @@ -8,7 +8,6 @@ import type { TicketedEvent } from '../types/ticket' import { ticketedEventToActivity } from '../types/activity' import { useActivitiesStore } from '../stores/activities' import { useActivityFilters } from './useActivityFilters' -import { useOwnedTickets } from './useOwnedTickets' /** * Main composable for activities discovery. @@ -18,7 +17,6 @@ export function useActivities() { const store = useActivitiesStore() const filters = useActivityFilters() const { isAuthenticated, currentUser } = useAuth() - const { ownedActivityIds } = useOwnedTickets() const isSubscribed = ref(false) const subscriptionError = ref(null) @@ -72,10 +70,7 @@ export function useActivities() { const all = store.activities.sort( (a, b) => a.startDate.getTime() - b.startDate.getTime() ) - const filtered = filters.applyFilters(all) - if (!filters.onlyOwnedTickets.value) return filtered - const owned = ownedActivityIds.value - return filtered.filter(a => owned.has(a.id)) + return filters.applyFilters(all) }) /** diff --git a/src/modules/activities/composables/useActivityFilters.ts b/src/modules/activities/composables/useActivityFilters.ts index 60bcb5e..ab3624b 100644 --- a/src/modules/activities/composables/useActivityFilters.ts +++ b/src/modules/activities/composables/useActivityFilters.ts @@ -15,13 +15,6 @@ export function useActivityFilters() { const temporal = ref(DEFAULT_FILTERS.temporal) const selectedCategories = ref([]) const selectedDate = ref(undefined) - /** - * When true, the feed is narrowed to activities the current user - * holds at least one paid ticket for. Crossed with the - * `ownedActivityIds` set from useOwnedTickets in useActivities - * (this composable stays free of ticket fetching). - */ - const onlyOwnedTickets = ref(false) const filters = computed(() => ({ temporal: temporal.value, @@ -88,18 +81,12 @@ export function useActivityFilters() { temporal.value = DEFAULT_FILTERS.temporal selectedCategories.value = [] selectedDate.value = undefined - onlyOwnedTickets.value = false - } - - function toggleOwnedTickets() { - onlyOwnedTickets.value = !onlyOwnedTickets.value } const hasActiveFilters = computed(() => temporal.value !== 'all' || selectedCategories.value.length > 0 || - selectedDate.value !== undefined || - onlyOwnedTickets.value + selectedDate.value !== undefined ) return { @@ -107,7 +94,6 @@ export function useActivityFilters() { temporal, selectedCategories, selectedDate, - onlyOwnedTickets, filters, hasActiveFilters, @@ -117,7 +103,6 @@ export function useActivityFilters() { selectDate, toggleCategory, clearCategories, - toggleOwnedTickets, resetFilters, } } diff --git a/src/modules/activities/composables/useOwnedTickets.ts b/src/modules/activities/composables/useOwnedTickets.ts deleted file mode 100644 index 5ea5d44..0000000 --- a/src/modules/activities/composables/useOwnedTickets.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { computed, ref, watch } from 'vue' -import { useAuth } from '@/composables/useAuthService' -import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import type { TicketApiService } from '../services/TicketApiService' -import type { ActivityTicket } from '../types/ticket' - -/** - * Module-level singleton: owned-ticket lookup keyed by activity id - * (== LNbits event id == NIP-52 d-tag, all the same string by - * extension contract). Lives at module scope so every - * + the detail page + the feed filter share ONE underlying fetch - * instead of each instance hitting the API. - * - * Auto-loads on first use after auth is ready, and re-loads when - * the current user changes (login/logout). Consumers that mutate the - * user's ticket set (e.g. a successful purchase) call `refresh()` - * directly so every surface reading this composable updates - * atomically. - */ - -const tickets = ref([]) -const isLoading = ref(false) -const error = ref(null) -let hasAutoLoaded = false -let lastLoadedUserId: string | null = null - -async function fetchTickets(): Promise { - const { isAuthenticated, currentUser } = useAuth() - if (!isAuthenticated.value || !currentUser.value) { - tickets.value = [] - lastLoadedUserId = null - return - } - - const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService - const lnbitsAPI = injectService(SERVICE_TOKENS.LNBITS_API) as any - const accessToken = lnbitsAPI?.getAccessToken?.() || '' - - isLoading.value = true - error.value = null - try { - tickets.value = await ticketApi.fetchUserTickets(currentUser.value.id, accessToken) - lastLoadedUserId = currentUser.value.id - } catch (e) { - error.value = e instanceof Error ? e : new Error(String(e)) - tickets.value = [] - } finally { - isLoading.value = false - } -} - -const ticketsByActivity = computed>(() => { - const m = new Map() - for (const ticket of tickets.value) { - const existing = m.get(ticket.activityId) - if (existing) { - existing.push(ticket) - } else { - m.set(ticket.activityId, [ticket]) - } - } - return m -}) - -const ownedActivityIds = computed>(() => { - const s = new Set() - for (const ticket of tickets.value) { - if (ticket.paid) s.add(ticket.activityId) - } - return s -}) - -function getTickets(activityId: string): ActivityTicket[] { - return ticketsByActivity.value.get(activityId) ?? [] -} - -function paidCount(activityId: string): number { - return getTickets(activityId).filter(t => t.paid).length -} - -export function useOwnedTickets() { - const { isAuthenticated, currentUser } = useAuth() - - // First call kicks off the initial load. Subsequent calls just - // attach to the shared state. - if (!hasAutoLoaded) { - hasAutoLoaded = true - fetchTickets() - - // Re-fetch when the current user changes (login / logout / - // account switch). Compares against the last-fetched user id - // so we don't re-fetch when other auth fields update (e.g. - // metadata refresh) without the user id changing. - watch( - () => currentUser.value?.id ?? null, - (id) => { - if (id !== lastLoadedUserId) fetchTickets() - }, - ) - } - - return { - tickets, - ticketsByActivity, - ownedActivityIds, - getTickets, - paidCount, - refresh: fetchTickets, - isLoading, - error, - isAuthenticated, - } -} diff --git a/src/modules/activities/types/activity.ts b/src/modules/activities/types/activity.ts index 993afa7..a377e8b 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, TicketTags } from './nip52' +import type { CalendarTimeEvent, CalendarDateEvent } from './nip52' import type { TicketedEvent } from './ticket' /** @@ -74,26 +74,8 @@ export interface OrganizerInfo { export interface ActivityTicketInfo { price: number currency: string - /** 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, - } + available: number + total: number } /** @@ -122,7 +104,6 @@ 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), } @@ -159,7 +140,6 @@ 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 b87db35..332d054 100644 --- a/src/modules/activities/types/nip52.ts +++ b/src/modules/activities/types/nip52.ts @@ -17,27 +17,6 @@ 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) */ @@ -57,7 +36,6 @@ export interface CalendarDateEvent { references: string[] id: string createdAt: number - ticket?: TicketTags } /** @@ -81,7 +59,6 @@ export interface CalendarTimeEvent { references: string[] id: string createdAt: number - ticket?: TicketTags } export interface Participant { @@ -119,35 +96,6 @@ 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. @@ -218,7 +166,6 @@ export function parseCalendarTimeEvent(event: NostrEvent): CalendarTimeEvent | n references: getTagValues(event.tags, 'r'), id: event.id, createdAt: event.created_at, - ticket: parseTicketTags(event.tags), } } @@ -266,7 +213,6 @@ export function parseCalendarDateEvent(event: NostrEvent): CalendarDateEvent | n references: getTagValues(event.tags, 'r'), id: event.id, createdAt: event.created_at, - ticket: parseTicketTags(event.tags), } } diff --git a/src/modules/activities/views/ActivitiesPage.vue b/src/modules/activities/views/ActivitiesPage.vue index d685a11..684aa97 100644 --- a/src/modules/activities/views/ActivitiesPage.vue +++ b/src/modules/activities/views/ActivitiesPage.vue @@ -8,9 +8,8 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' -import { SlidersHorizontal, ChevronDown, Ticket } from 'lucide-vue-next' +import { SlidersHorizontal, ChevronDown } from 'lucide-vue-next' import { useActivities } from '../composables/useActivities' -import { useAuth } from '@/composables/useAuthService' import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue' import TemporalFilterBar from '../components/TemporalFilterBar.vue' import CategoryFilterBar from '../components/CategoryFilterBar.vue' @@ -29,18 +28,14 @@ const { selectedCategories, hasActiveFilters, selectedDate, - onlyOwnedTickets, selectDate, setTemporal, toggleCategory, clearCategories, - toggleOwnedTickets, resetFilters, subscribe, } = useActivities() -const { isAuthenticated } = useAuth() - const filtersOpen = ref(false) onMounted(() => { @@ -79,21 +74,6 @@ function handleSelectActivity(activity: Activity) { - -
- -
- diff --git a/src/modules/activities/views/ActivityDetailPage.vue b/src/modules/activities/views/ActivityDetailPage.vue index c347363..1929ca2 100644 --- a/src/modules/activities/views/ActivityDetailPage.vue +++ b/src/modules/activities/views/ActivityDetailPage.vue @@ -8,18 +8,15 @@ import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' import { - Calendar, MapPin, ArrowLeft, Pencil, Ticket, CheckCircle2, + Calendar, MapPin, ArrowLeft, Pencil, } from 'lucide-vue-next' import { useActivityDetail } from '../composables/useActivityDetail' import BookmarkButton from '../components/BookmarkButton.vue' import RSVPButton from '../components/RSVPButton.vue' import OrganizerCard from '../components/OrganizerCard.vue' -import PurchaseTicketDialog from '../components/PurchaseTicketDialog.vue' import { NIP52_KINDS } from '../types/nip52' import { useAuth } from '@/composables/useAuthService' import { useActivitiesStore } from '../stores/activities' -import { useOwnedTickets } from '../composables/useOwnedTickets' -import { toastService } from '@/core/services/ToastService' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { TicketApiService } from '../services/TicketApiService' import type { TicketedEvent } from '../types/ticket' @@ -97,56 +94,6 @@ const categoryLabel = computed(() => { function goBack() { router.push({ name: 'activities' }) } - -// --- Ticket purchase + owned-tickets surface ---------------------- - -const { getTickets, paidCount, refresh: refreshOwnedTickets } = useOwnedTickets() - -const ownedTicketsForActivity = computed(() => getTickets(activityId)) -const ownedPaidCount = computed(() => paidCount(activityId)) - -const purchaseEvent = computed(() => { - const a = activity.value - if (!a || !a.ticketInfo) return null - return { - id: a.id, - name: a.title, - price_per_ticket: a.ticketInfo.price, - currency: a.ticketInfo.currency, - allow_fiat: a.ticketInfo.allowFiat, - fiat_currency: a.ticketInfo.fiatCurrency, - } -}) - -// available === undefined → unlimited capacity, button always shown -// available === 0 → sold out, button hidden -// available > 0 → button shown -const canBuyTicket = computed(() => { - const info = activity.value?.ticketInfo - if (!info) return false - return info.available === undefined || info.available > 0 -}) - -const showPurchaseDialog = ref(false) - -function openPurchaseDialog() { - if (!isAuthenticated.value) { - toastService.info('Log in to buy tickets') - return - } - showPurchaseDialog.value = true -} - -// Re-fetch the user's tickets when the purchase dialog closes (the -// buyer may have just paid). The inventory side updates automatically -// via the relay republish from the events extension. -watch(showPurchaseDialog, (open) => { - if (!open) refreshOwnedTickets() -}) - -function goToMyTickets() { - router.push('/my-tickets') -}