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 722bc21f4d
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 { 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))
}) })
/** /**

View file

@ -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,
} }
} }

View file

@ -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>