diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 38bb64a..0c529a2 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -58,6 +58,7 @@ const messages: LocaleMessages = { thisWeek: 'This Week', thisMonth: 'This Month', myTickets: 'My tickets', + hosting: 'Hosting', }, categories: { concert: 'Concert', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index b2603e9..e72be71 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -58,6 +58,7 @@ const messages: LocaleMessages = { thisWeek: 'Esta semana', thisMonth: 'Este mes', myTickets: 'Mis boletos', + hosting: 'Organizo', }, categories: { concert: 'Concierto', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 388f602..3b6ed76 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -58,6 +58,7 @@ const messages: LocaleMessages = { thisWeek: 'Cette semaine', thisMonth: 'Ce mois-ci', myTickets: 'Mes billets', + hosting: 'J\'organise', }, categories: { concert: 'Concert', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index d44f236..4e5584d 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -59,6 +59,7 @@ export interface LocaleMessages { thisWeek: string thisMonth: string myTickets: string + hosting: string } categories: Record detail: { diff --git a/src/modules/activities/composables/useActivityFilters.ts b/src/modules/activities/composables/useActivityFilters.ts index 60bcb5e..4aba255 100644 --- a/src/modules/activities/composables/useActivityFilters.ts +++ b/src/modules/activities/composables/useActivityFilters.ts @@ -22,6 +22,13 @@ export function useActivityFilters() { * (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) const filters = computed(() => ({ temporal: temporal.value, @@ -54,6 +61,13 @@ export function useActivityFilters() { ) } + // Hosting filter — activities the signed-in user organizes. + // Read off `activity.isMine` which `useActivities.tagOwnership()` + // populates from organizer-pubkey match + LNbits drafts. + if (onlyHosting.value) { + result = result.filter(a => a.isMine === true) + } + return result } @@ -89,17 +103,23 @@ export function useActivityFilters() { selectedCategories.value = [] selectedDate.value = undefined onlyOwnedTickets.value = false + onlyHosting.value = false } function toggleOwnedTickets() { onlyOwnedTickets.value = !onlyOwnedTickets.value } + function toggleHosting() { + onlyHosting.value = !onlyHosting.value + } + const hasActiveFilters = computed(() => temporal.value !== 'all' || selectedCategories.value.length > 0 || selectedDate.value !== undefined || - onlyOwnedTickets.value + onlyOwnedTickets.value || + onlyHosting.value ) return { @@ -108,6 +128,7 @@ export function useActivityFilters() { selectedCategories, selectedDate, onlyOwnedTickets, + onlyHosting, filters, hasActiveFilters, @@ -118,6 +139,7 @@ export function useActivityFilters() { toggleCategory, clearCategories, toggleOwnedTickets, + toggleHosting, resetFilters, } } diff --git a/src/modules/activities/composables/useTicketScanner.ts b/src/modules/activities/composables/useTicketScanner.ts index 638b8bc..50006a1 100644 --- a/src/modules/activities/composables/useTicketScanner.ts +++ b/src/modules/activities/composables/useTicketScanner.ts @@ -39,6 +39,17 @@ export function useTicketScanner(activityId: Ref) { const isProcessing = ref(false) const lastScan = ref(null) + /** + * Set to `true` immediately after a decode resolves (success or + * failure) and stays true until the operator dismisses or taps + * "Scan next". While paused, further `onDecode` calls are dropped + * — the camera keeps streaming for instant resume but the result + * banner sticks so the door can confirm the outcome before + * moving on. Without this, the 5-fps decode loop instantly + * fires "already scanned this session" on the very ticket that + * just succeeded. + */ + const isPaused = ref(false) const scanned = useLocalStorage( () => `activities_scanned_${activityId.value}`, [], @@ -51,7 +62,7 @@ export function useTicketScanner(activityId: Ref) { } async function onDecode(qrText: string): Promise { - if (isProcessing.value) return + if (isProcessing.value || isPaused.value) return const ticketId = parseTicketId(qrText).trim() if (!ticketId) return @@ -60,6 +71,7 @@ export function useTicketScanner(activityId: Ref) { // of the camera for multiple decode frames. if (scanned.value.some(r => r.ticketId === ticketId)) { lastScan.value = { status: 'duplicate-session', ticketId } + isPaused.value = true return } @@ -90,24 +102,32 @@ export function useTicketScanner(activityId: Ref) { } } finally { isProcessing.value = false + // Pause the decode loop regardless of outcome. The operator + // taps "Scan next" to resume; this guarantees they see the + // banner and can correct (let the attendee in, deny entry, + // etc.) before the next QR comes into frame. + isPaused.value = true } } + function resume() { + lastScan.value = null + isPaused.value = false + } + function clearScanned() { scanned.value = [] lastScan.value = null - } - - function dismissLastScan() { - lastScan.value = null + isPaused.value = false } return { isProcessing, + isPaused, lastScan, scanned, onDecode, + resume, clearScanned, - dismissLastScan, } } diff --git a/src/modules/activities/views/ActivitiesPage.vue b/src/modules/activities/views/ActivitiesPage.vue index d685a11..ccfc08e 100644 --- a/src/modules/activities/views/ActivitiesPage.vue +++ b/src/modules/activities/views/ActivitiesPage.vue @@ -8,7 +8,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' -import { SlidersHorizontal, ChevronDown, Ticket } from 'lucide-vue-next' +import { SlidersHorizontal, ChevronDown, Ticket, Megaphone } from 'lucide-vue-next' import { useActivities } from '../composables/useActivities' import { useAuth } from '@/composables/useAuthService' import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue' @@ -30,11 +30,13 @@ const { hasActiveFilters, selectedDate, onlyOwnedTickets, + onlyHosting, selectDate, setTemporal, toggleCategory, clearCategories, toggleOwnedTickets, + toggleHosting, resetFilters, subscribe, } = useActivities() @@ -79,10 +81,10 @@ function handleSelectActivity(activity: Activity) { - -
+ +
+
diff --git a/src/modules/activities/views/ScanTicketsPage.vue b/src/modules/activities/views/ScanTicketsPage.vue index 4a2d74e..67c51f5 100644 --- a/src/modules/activities/views/ScanTicketsPage.vue +++ b/src/modules/activities/views/ScanTicketsPage.vue @@ -1,7 +1,7 @@