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:
parent
5ed0d6da9e
commit
722bc21f4d
3 changed files with 43 additions and 3 deletions
|
|
@ -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))
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue