diff --git a/src/modules/activities/components/ActivityCard.vue b/src/modules/activities/components/ActivityCard.vue index 7c156e6..6247635 100644 --- a/src/modules/activities/components/ActivityCard.vue +++ b/src/modules/activities/components/ActivityCard.vue @@ -12,6 +12,10 @@ import type { Activity } from '../types/activity' const props = defineProps<{ activity: Activity + /** Render a compact row: no hero image, no summary, single-line + * title, tighter padding. Used by the Hosting view where the + * host already knows what their events look like. */ + compact?: boolean }>() const emit = defineEmits<{ @@ -58,19 +62,46 @@ const isPast = computed(() => { if (!end || isNaN(end.getTime())) return false return end.getTime() < Date.now() }) + +// Pending / rejected events get a washed-out look so the user +// sees at a glance the event isn't live, not just the small badge. +const isNonApproved = computed( + () => !!props.activity.lnbitsStatus && props.activity.lnbitsStatus !== 'approved', +) diff --git a/src/modules/activities/components/ActivityList.vue b/src/modules/activities/components/ActivityList.vue index 726f09e..45a0567 100644 --- a/src/modules/activities/components/ActivityList.vue +++ b/src/modules/activities/components/ActivityList.vue @@ -7,6 +7,10 @@ import type { Activity } from '../types/activity' defineProps<{ activities: Activity[] isLoading?: boolean + /** Render compact rows instead of full-image cards. Used by the + * Hosting view so an operator can scan their roster of events + * without the visual weight of hero images they already recognize. */ + compact?: boolean }>() const emit = defineEmits<{ @@ -39,20 +43,24 @@ const { t } = useI18n() class="flex flex-col items-center justify-center py-16 text-center" > -

+

{{ t('activities.noActivities') }}

-

- {{ t('activities.search.noResults') }} -

- -
+ +
diff --git a/src/modules/activities/components/PurchaseTicketDialog.vue b/src/modules/activities/components/PurchaseTicketDialog.vue index b5b5d3f..6617496 100644 --- a/src/modules/activities/components/PurchaseTicketDialog.vue +++ b/src/modules/activities/components/PurchaseTicketDialog.vue @@ -443,7 +443,7 @@ onUnmounted(() => {
diff --git a/src/modules/activities/composables/useActivityFilters.ts b/src/modules/activities/composables/useActivityFilters.ts index 8f9914b..a5ae1da 100644 --- a/src/modules/activities/composables/useActivityFilters.ts +++ b/src/modules/activities/composables/useActivityFilters.ts @@ -8,35 +8,22 @@ import type { ActivityCategory } from '../types/category' import type { TemporalFilter, ActivityFilters } from '../types/filters' import { DEFAULT_FILTERS } from '../types/filters' +// Filter state is hoisted to module scope so every `useActivities()` / +// `useActivityFilters()` call shares the same refs. The bottom-nav +// Hosting tab in activities-app/App.vue and the feed view in +// ActivitiesPage.vue both rely on this — without a shared instance, +// tapping Hosting toggled a private ref the page never saw. +const temporal = ref(DEFAULT_FILTERS.temporal) +const selectedCategories = ref([]) +const selectedDate = ref(undefined) +const onlyOwnedTickets = ref(false) +const onlyHosting = ref(false) +const showPast = ref(false) + /** * Composable for managing activity filter state and applying filters reactively. */ 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) - /** - * When true, the feed is narrowed to activities the current user - * is hosting (organizer pubkey matches the signed-in user, or the - * row is a local LNbits draft of theirs). Reads `activity.isMine` - * which `useActivities.tagOwnership()` populates. - */ - const onlyHosting = ref(false) - /** - * When false (default), activities that have already ended are - * hidden from the feed. Toggling on includes them so the user can - * browse past events. The date-picker overrides this — picking a - * specific past date shows that day's activities regardless, - * mirroring how it overrides the temporal pills. - */ - const showPast = ref(false) const filters = computed(() => ({ temporal: temporal.value, diff --git a/src/modules/activities/composables/useTicketScanner.ts b/src/modules/activities/composables/useTicketScanner.ts index 4538d3e..3e4647d 100644 --- a/src/modules/activities/composables/useTicketScanner.ts +++ b/src/modules/activities/composables/useTicketScanner.ts @@ -188,6 +188,38 @@ export function useTicketScanner(activityId: Ref) { isPaused.value = false } + /** + * Mark a ticket as registered without going through the camera — + * used when the host knows the attendee in person or accepts an + * alternate proof of identity. Same backend endpoint as a scan + * (so it also gates on event ownership and rejects unpaid / + * already-registered tickets), but skips the scanner pause + + * full-screen banner since the operator initiated the action + * from the roster directly. Refreshes stats on success. + */ + async function registerManually( + ticketId: string, + ): Promise<{ ok: boolean; error?: string }> { + const adminKey = currentUser.value?.wallets?.[0]?.adminkey + if (!adminKey) return { ok: false, error: 'No wallet admin key available' } + try { + await ticketApi.registerTicket(ticketId, adminKey) + // Mirror the session-local dedup the scan path uses so a + // subsequent QR scan of the same ticket reports "Already + // scanned" instead of round-tripping a duplicate register. + if (!scanned.value.some(r => r.ticketId === ticketId)) { + scanned.value = [ + { ticketId, name: null, registeredAt: new Date().toISOString() }, + ...scanned.value, + ] + } + await refreshStats() + return { ok: true } + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : String(e) } + } + } + function clearScanned() { scanned.value = [] lastScan.value = null @@ -210,5 +242,6 @@ export function useTicketScanner(activityId: Ref) { onDecode, resume, clearScanned, + registerManually, } } diff --git a/src/modules/activities/views/ActivitiesCalendarPage.vue b/src/modules/activities/views/ActivitiesCalendarPage.vue index c56b12a..3c4eccf 100644 --- a/src/modules/activities/views/ActivitiesCalendarPage.vue +++ b/src/modules/activities/views/ActivitiesCalendarPage.vue @@ -1,12 +1,31 @@