feat(activities): ticket purchase + Nostr-driven inventory sync #71
3 changed files with 43 additions and 3 deletions
feat(activities): "My tickets" filter chip on ActivitiesPage
A new filter chip sits below the temporal pills, hidden when the user is logged out. Clicking it narrows the feed to activities the user holds at least one paid ticket for — intersecting the existing filter pipeline (temporal / categories / date) with the ownedActivityIds set from useOwnedTickets. The coupling lives in useActivities (it already orchestrates the data + filter pipeline). useActivityFilters stays free of ticket fetching; it just carries the boolean state. resetFilters clears the chip alongside the other filters, and hasActiveFilters lights up when it's on so the "Clear all" affordance is visible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit
ea4e1960f5
|
|
@ -8,6 +8,7 @@ import type { TicketedEvent } from '../types/ticket'
|
||||||
import { ticketedEventToActivity } from '../types/activity'
|
import { ticketedEventToActivity } from '../types/activity'
|
||||||
import { useActivitiesStore } from '../stores/activities'
|
import { useActivitiesStore } from '../stores/activities'
|
||||||
import { useActivityFilters } from './useActivityFilters'
|
import { useActivityFilters } from './useActivityFilters'
|
||||||
|
import { useOwnedTickets } from './useOwnedTickets'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main composable for activities discovery.
|
* Main composable for activities discovery.
|
||||||
|
|
@ -17,6 +18,7 @@ export function useActivities() {
|
||||||
const store = useActivitiesStore()
|
const store = useActivitiesStore()
|
||||||
const filters = useActivityFilters()
|
const filters = useActivityFilters()
|
||||||
const { isAuthenticated, currentUser } = useAuth()
|
const { isAuthenticated, currentUser } = useAuth()
|
||||||
|
const { ownedActivityIds } = useOwnedTickets()
|
||||||
|
|
||||||
const isSubscribed = ref(false)
|
const isSubscribed = ref(false)
|
||||||
const subscriptionError = ref<string | null>(null)
|
const subscriptionError = ref<string | null>(null)
|
||||||
|
|
@ -70,7 +72,10 @@ export function useActivities() {
|
||||||
const all = store.activities.sort(
|
const all = store.activities.sort(
|
||||||
(a, b) => a.startDate.getTime() - b.startDate.getTime()
|
(a, b) => a.startDate.getTime() - b.startDate.getTime()
|
||||||
)
|
)
|
||||||
return filters.applyFilters(all)
|
const filtered = filters.applyFilters(all)
|
||||||
|
if (!filters.onlyOwnedTickets.value) return filtered
|
||||||
|
const owned = ownedActivityIds.value
|
||||||
|
return filtered.filter(a => owned.has(a.id))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ export function useActivityFilters() {
|
||||||
const temporal = ref<TemporalFilter>(DEFAULT_FILTERS.temporal)
|
const temporal = ref<TemporalFilter>(DEFAULT_FILTERS.temporal)
|
||||||
const selectedCategories = ref<ActivityCategory[]>([])
|
const selectedCategories = ref<ActivityCategory[]>([])
|
||||||
const selectedDate = ref<Date | undefined>(undefined)
|
const selectedDate = ref<Date | undefined>(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<ActivityFilters>(() => ({
|
const filters = computed<ActivityFilters>(() => ({
|
||||||
temporal: temporal.value,
|
temporal: temporal.value,
|
||||||
|
|
@ -81,12 +88,18 @@ export function useActivityFilters() {
|
||||||
temporal.value = DEFAULT_FILTERS.temporal
|
temporal.value = DEFAULT_FILTERS.temporal
|
||||||
selectedCategories.value = []
|
selectedCategories.value = []
|
||||||
selectedDate.value = undefined
|
selectedDate.value = undefined
|
||||||
|
onlyOwnedTickets.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOwnedTickets() {
|
||||||
|
onlyOwnedTickets.value = !onlyOwnedTickets.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasActiveFilters = computed(() =>
|
const hasActiveFilters = computed(() =>
|
||||||
temporal.value !== 'all' ||
|
temporal.value !== 'all' ||
|
||||||
selectedCategories.value.length > 0 ||
|
selectedCategories.value.length > 0 ||
|
||||||
selectedDate.value !== undefined
|
selectedDate.value !== undefined ||
|
||||||
|
onlyOwnedTickets.value
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -94,6 +107,7 @@ export function useActivityFilters() {
|
||||||
temporal,
|
temporal,
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
|
onlyOwnedTickets,
|
||||||
filters,
|
filters,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
|
|
||||||
|
|
@ -103,6 +117,7 @@ export function useActivityFilters() {
|
||||||
selectDate,
|
selectDate,
|
||||||
toggleCategory,
|
toggleCategory,
|
||||||
clearCategories,
|
clearCategories,
|
||||||
|
toggleOwnedTickets,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import { SlidersHorizontal, ChevronDown } from 'lucide-vue-next'
|
import { SlidersHorizontal, ChevronDown, Ticket } from 'lucide-vue-next'
|
||||||
import { useActivities } from '../composables/useActivities'
|
import { useActivities } from '../composables/useActivities'
|
||||||
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue'
|
import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue'
|
||||||
import TemporalFilterBar from '../components/TemporalFilterBar.vue'
|
import TemporalFilterBar from '../components/TemporalFilterBar.vue'
|
||||||
import CategoryFilterBar from '../components/CategoryFilterBar.vue'
|
import CategoryFilterBar from '../components/CategoryFilterBar.vue'
|
||||||
|
|
@ -28,14 +29,18 @@ const {
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
|
onlyOwnedTickets,
|
||||||
selectDate,
|
selectDate,
|
||||||
setTemporal,
|
setTemporal,
|
||||||
toggleCategory,
|
toggleCategory,
|
||||||
clearCategories,
|
clearCategories,
|
||||||
|
toggleOwnedTickets,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
subscribe,
|
subscribe,
|
||||||
} = useActivities()
|
} = useActivities()
|
||||||
|
|
||||||
|
const { isAuthenticated } = useAuth()
|
||||||
|
|
||||||
const filtersOpen = ref(false)
|
const filtersOpen = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -74,6 +79,21 @@ function handleSelectActivity(activity: Activity) {
|
||||||
<TemporalFilterBar :model-value="temporal" @update:model-value="setTemporal" />
|
<TemporalFilterBar :model-value="temporal" @update:model-value="setTemporal" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- "My tickets" filter chip — narrows the feed to activities
|
||||||
|
the user holds at least one paid ticket for. Hidden when
|
||||||
|
logged out (no tickets to filter on). -->
|
||||||
|
<div v-if="isAuthenticated" class="mb-4">
|
||||||
|
<Button
|
||||||
|
:variant="onlyOwnedTickets ? 'default' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
class="gap-1.5"
|
||||||
|
@click="toggleOwnedTickets"
|
||||||
|
>
|
||||||
|
<Ticket class="w-3.5 h-3.5" />
|
||||||
|
{{ t('activities.filters.myTickets', 'My tickets') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Category filters (collapsible) -->
|
<!-- Category filters (collapsible) -->
|
||||||
<Collapsible v-model:open="filtersOpen" class="mb-6">
|
<Collapsible v-model:open="filtersOpen" class="mb-6">
|
||||||
<CollapsibleTrigger as-child>
|
<CollapsibleTrigger as-child>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue