feat(activities): "Hosting" filter chip on the activities feed

Companion to the "My tickets" chip from #71. Where "My tickets"
narrows the feed to events you're attending, "Hosting" narrows it
to events you're organizing — reading `activity.isMine` which
useActivities.tagOwnership() already populates from organizer
pubkey match + own LNbits drafts.

Naming rationale: "My events" would have been ambiguous with
favorited / bookmarked. "Hosting" is short, role-oriented, and
pairs as the natural counterpart to "My tickets" (attending vs.
organizing). Spanish/French translations lean on the verb form
("Organizo" / "J'organise") since those languages don't have a
clean noun equivalent.

- useActivityFilters: onlyHosting flag, toggleHosting action,
  resetFilters clears it, hasActiveFilters lights up.
- applyFilters filters by `a.isMine === true` when the flag is
  on. Composes with category / temporal / "My tickets" via the
  same intersection chain.
- ActivitiesPage: chip rendered alongside "My tickets" with the
  Megaphone icon (lucide). Hidden when logged out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-24 17:39:31 +02:00
commit 5ebf0582e0
6 changed files with 43 additions and 6 deletions

View file

@ -58,6 +58,7 @@ const messages: LocaleMessages = {
thisWeek: 'This Week',
thisMonth: 'This Month',
myTickets: 'My tickets',
hosting: 'Hosting',
},
categories: {
concert: 'Concert',

View file

@ -58,6 +58,7 @@ const messages: LocaleMessages = {
thisWeek: 'Esta semana',
thisMonth: 'Este mes',
myTickets: 'Mis boletos',
hosting: 'Organizo',
},
categories: {
concert: 'Concierto',

View file

@ -58,6 +58,7 @@ const messages: LocaleMessages = {
thisWeek: 'Cette semaine',
thisMonth: 'Ce mois-ci',
myTickets: 'Mes billets',
hosting: 'J\'organise',
},
categories: {
concert: 'Concert',

View file

@ -59,6 +59,7 @@ export interface LocaleMessages {
thisWeek: string
thisMonth: string
myTickets: string
hosting: string
}
categories: Record<string, string>
detail: {

View file

@ -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<ActivityFilters>(() => ({
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,
}
}

View file

@ -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) {
<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">
<!-- Role filter chips narrow the feed to activities the user
has skin in. Hidden when logged out (nothing to filter on).
"My tickets" = attending; "Hosting" = organizing. -->
<div v-if="isAuthenticated" class="mb-4 flex flex-wrap gap-2">
<Button
:variant="onlyOwnedTickets ? 'default' : 'outline'"
size="sm"
@ -92,6 +94,15 @@ function handleSelectActivity(activity: Activity) {
<Ticket class="w-3.5 h-3.5" />
{{ t('activities.filters.myTickets', 'My tickets') }}
</Button>
<Button
:variant="onlyHosting ? 'default' : 'outline'"
size="sm"
class="gap-1.5"
@click="toggleHosting"
>
<Megaphone class="w-3.5 h-3.5" />
{{ t('activities.filters.hosting', 'Hosting') }}
</Button>
</div>
<!-- Category filters (collapsible) -->