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>
This commit is contained in:
Padreug 2026-05-23 20:46:42 +02:00
commit ea4e1960f5
3 changed files with 43 additions and 3 deletions

View file

@ -8,6 +8,7 @@ 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.
@ -17,6 +18,7 @@ export function useActivities() {
const store = useActivitiesStore()
const filters = useActivityFilters()
const { isAuthenticated, currentUser } = useAuth()
const { ownedActivityIds } = useOwnedTickets()
const isSubscribed = ref(false)
const subscriptionError = ref<string | null>(null)
@ -70,7 +72,10 @@ export function useActivities() {
const all = store.activities.sort(
(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))
})
/**

View file

@ -15,6 +15,13 @@ export function useActivityFilters() {
const temporal = ref<TemporalFilter>(DEFAULT_FILTERS.temporal)
const selectedCategories = ref<ActivityCategory[]>([])
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>(() => ({
temporal: temporal.value,
@ -81,12 +88,18 @@ 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
selectedDate.value !== undefined ||
onlyOwnedTickets.value
)
return {
@ -94,6 +107,7 @@ export function useActivityFilters() {
temporal,
selectedCategories,
selectedDate,
onlyOwnedTickets,
filters,
hasActiveFilters,
@ -103,6 +117,7 @@ export function useActivityFilters() {
selectDate,
toggleCategory,
clearCategories,
toggleOwnedTickets,
resetFilters,
}
}

View file

@ -8,8 +8,9 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} 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 { useAuth } from '@/composables/useAuthService'
import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue'
import TemporalFilterBar from '../components/TemporalFilterBar.vue'
import CategoryFilterBar from '../components/CategoryFilterBar.vue'
@ -28,14 +29,18 @@ const {
selectedCategories,
hasActiveFilters,
selectedDate,
onlyOwnedTickets,
selectDate,
setTemporal,
toggleCategory,
clearCategories,
toggleOwnedTickets,
resetFilters,
subscribe,
} = useActivities()
const { isAuthenticated } = useAuth()
const filtersOpen = ref(false)
onMounted(() => {
@ -74,6 +79,21 @@ function handleSelectActivity(activity: Activity) {
<TemporalFilterBar :model-value="temporal" @update:model-value="setTemporal" />
</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) -->
<Collapsible v-model:open="filtersOpen" class="mb-6">
<CollapsibleTrigger as-child>