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:
parent
f3c8b1cf95
commit
9acb61efcf
6 changed files with 43 additions and 6 deletions
|
|
@ -58,6 +58,7 @@ const messages: LocaleMessages = {
|
||||||
thisWeek: 'This Week',
|
thisWeek: 'This Week',
|
||||||
thisMonth: 'This Month',
|
thisMonth: 'This Month',
|
||||||
myTickets: 'My tickets',
|
myTickets: 'My tickets',
|
||||||
|
hosting: 'Hosting',
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
concert: 'Concert',
|
concert: 'Concert',
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ const messages: LocaleMessages = {
|
||||||
thisWeek: 'Esta semana',
|
thisWeek: 'Esta semana',
|
||||||
thisMonth: 'Este mes',
|
thisMonth: 'Este mes',
|
||||||
myTickets: 'Mis boletos',
|
myTickets: 'Mis boletos',
|
||||||
|
hosting: 'Organizo',
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
concert: 'Concierto',
|
concert: 'Concierto',
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ const messages: LocaleMessages = {
|
||||||
thisWeek: 'Cette semaine',
|
thisWeek: 'Cette semaine',
|
||||||
thisMonth: 'Ce mois-ci',
|
thisMonth: 'Ce mois-ci',
|
||||||
myTickets: 'Mes billets',
|
myTickets: 'Mes billets',
|
||||||
|
hosting: 'J\'organise',
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
concert: 'Concert',
|
concert: 'Concert',
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export interface LocaleMessages {
|
||||||
thisWeek: string
|
thisWeek: string
|
||||||
thisMonth: string
|
thisMonth: string
|
||||||
myTickets: string
|
myTickets: string
|
||||||
|
hosting: string
|
||||||
}
|
}
|
||||||
categories: Record<string, string>
|
categories: Record<string, string>
|
||||||
detail: {
|
detail: {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@ export function useActivityFilters() {
|
||||||
* (this composable stays free of ticket fetching).
|
* (this composable stays free of ticket fetching).
|
||||||
*/
|
*/
|
||||||
const onlyOwnedTickets = ref(false)
|
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>(() => ({
|
const filters = computed<ActivityFilters>(() => ({
|
||||||
temporal: temporal.value,
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,17 +103,23 @@ export function useActivityFilters() {
|
||||||
selectedCategories.value = []
|
selectedCategories.value = []
|
||||||
selectedDate.value = undefined
|
selectedDate.value = undefined
|
||||||
onlyOwnedTickets.value = false
|
onlyOwnedTickets.value = false
|
||||||
|
onlyHosting.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleOwnedTickets() {
|
function toggleOwnedTickets() {
|
||||||
onlyOwnedTickets.value = !onlyOwnedTickets.value
|
onlyOwnedTickets.value = !onlyOwnedTickets.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleHosting() {
|
||||||
|
onlyHosting.value = !onlyHosting.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
|
onlyOwnedTickets.value ||
|
||||||
|
onlyHosting.value
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -108,6 +128,7 @@ export function useActivityFilters() {
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
onlyOwnedTickets,
|
onlyOwnedTickets,
|
||||||
|
onlyHosting,
|
||||||
filters,
|
filters,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
|
|
||||||
|
|
@ -118,6 +139,7 @@ export function useActivityFilters() {
|
||||||
toggleCategory,
|
toggleCategory,
|
||||||
clearCategories,
|
clearCategories,
|
||||||
toggleOwnedTickets,
|
toggleOwnedTickets,
|
||||||
|
toggleHosting,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} 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 { useActivities } from '../composables/useActivities'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue'
|
import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue'
|
||||||
|
|
@ -30,11 +30,13 @@ const {
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
onlyOwnedTickets,
|
onlyOwnedTickets,
|
||||||
|
onlyHosting,
|
||||||
selectDate,
|
selectDate,
|
||||||
setTemporal,
|
setTemporal,
|
||||||
toggleCategory,
|
toggleCategory,
|
||||||
clearCategories,
|
clearCategories,
|
||||||
toggleOwnedTickets,
|
toggleOwnedTickets,
|
||||||
|
toggleHosting,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
subscribe,
|
subscribe,
|
||||||
} = useActivities()
|
} = useActivities()
|
||||||
|
|
@ -79,10 +81,10 @@ 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
|
<!-- Role filter chips — narrow the feed to activities the user
|
||||||
the user holds at least one paid ticket for. Hidden when
|
has skin in. Hidden when logged out (nothing to filter on).
|
||||||
logged out (no tickets to filter on). -->
|
"My tickets" = attending; "Hosting" = organizing. -->
|
||||||
<div v-if="isAuthenticated" class="mb-4">
|
<div v-if="isAuthenticated" class="mb-4 flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
:variant="onlyOwnedTickets ? 'default' : 'outline'"
|
:variant="onlyOwnedTickets ? 'default' : 'outline'"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -92,6 +94,15 @@ function handleSelectActivity(activity: Activity) {
|
||||||
<Ticket class="w-3.5 h-3.5" />
|
<Ticket class="w-3.5 h-3.5" />
|
||||||
{{ t('activities.filters.myTickets', 'My tickets') }}
|
{{ t('activities.filters.myTickets', 'My tickets') }}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Category filters (collapsible) -->
|
<!-- Category filters (collapsible) -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue