feat(webapp): replace HubPill with hamburger sidebar menu
The top-right "Back to hub" pill in each standalone is replaced by a hamburger button that opens a right-side sheet reusing the existing ProfileSheetContent (identity card, back-to-hub link, theme/lang/ currency prefs, profile settings or log-in CTA). The redundant Profile entry is removed from BottomNav and its loggedOutOpensSheet plumbing (BottomNav → AppShell) is dropped — Hub.vue still mounts ProfileSheetTrigger directly so it's unaffected. ProfileSheetContent gains an `app-nav` slot so standalones can inject app-specific nav items above the cross-app section. AppShell exposes a new optional `sidebarNav` prop that forwards items to the menu; unset on non-activities standalones, those still get the hamburger menu showing just the shared profile/preferences content. Activities passes "My tickets" (routes to /my-tickets) and "Hosting" (toggles the onlyHosting feed filter and lands on /activities), so those entries leave the inline filter chip row on ActivitiesPage and live in the sidebar instead. The "Past events" chip stays inline — it doesn't require auth and pairs visually with the temporal filters.
This commit is contained in:
parent
6885b64ef2
commit
ac83a61eb1
11 changed files with 135 additions and 79 deletions
|
|
@ -4,24 +4,23 @@ import { useRoute } from 'vue-router'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
import { useTheme } from '@/components/theme-provider'
|
import { useTheme } from '@/components/theme-provider'
|
||||||
import BottomNav, { type BottomTab } from './BottomNav.vue'
|
import BottomNav, { type BottomTab } from './BottomNav.vue'
|
||||||
import HubPill from './HubPill.vue'
|
import StandaloneMenu, { type SidebarNavItem } from './StandaloneMenu.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** App-specific tabs displayed before the constant Profile entry. */
|
/** App-specific tabs displayed before the constant Profile entry. */
|
||||||
tabs: BottomTab[]
|
tabs: BottomTab[]
|
||||||
/** Active-tab matcher. Forwarded to BottomNav. */
|
/** Active-tab matcher. Forwarded to BottomNav. */
|
||||||
isActive: (path: string) => boolean
|
isActive: (path: string) => boolean
|
||||||
/** Hide the top-right HubPill — only true when this shell is rendering
|
/** Hide the top-right standalone menu — only true when this shell is
|
||||||
* the hub itself. Standalones leave this false (default). */
|
* rendering the hub itself. Standalones leave this false (default). */
|
||||||
hideHub?: boolean
|
hideHub?: boolean
|
||||||
/** Forwarded to BottomNav. Hub passes true so logged-out users can still
|
/** App-specific nav items rendered at the top of the standalone menu. */
|
||||||
* reach prefs from the sheet. Standalones leave it false. */
|
sidebarNav?: SidebarNavItem[]
|
||||||
loggedOutOpensSheet?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
hideHub: false,
|
hideHub: false,
|
||||||
loggedOutOpensSheet: false,
|
sidebarNav: () => [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
@ -45,11 +44,13 @@ const isLoginPage = computed(() => route.path === '/login')
|
||||||
v-if="!isLoginPage"
|
v-if="!isLoginPage"
|
||||||
:tabs="props.tabs"
|
:tabs="props.tabs"
|
||||||
:is-active="props.isActive"
|
:is-active="props.isActive"
|
||||||
:logged-out-opens-sheet="props.loggedOutOpensSheet"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HubPill v-if="!props.hideHub && !isLoginPage" />
|
<StandaloneMenu
|
||||||
|
v-if="!props.hideHub && !isLoginPage"
|
||||||
|
:items="props.sidebarNav"
|
||||||
|
/>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
||||||
<!-- Default slot for shell-level overlays (dialogs, sheets, etc.) that
|
<!-- Default slot for shell-level overlays (dialogs, sheets, etc.) that
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import ProfileSheetTrigger from './ProfileSheetTrigger.vue'
|
|
||||||
|
|
||||||
export interface BottomTab {
|
export interface BottomTab {
|
||||||
/** Translated label shown under the icon. */
|
/** Translated label shown under the icon. */
|
||||||
|
|
@ -25,13 +24,9 @@ interface Props {
|
||||||
/** Active-tab matcher. Each app has its own nesting rules so we don't try
|
/** Active-tab matcher. Each app has its own nesting rules so we don't try
|
||||||
* to derive a one-size-fits-all default — consumer supplies the function. */
|
* to derive a one-size-fits-all default — consumer supplies the function. */
|
||||||
isActive: (path: string) => boolean
|
isActive: (path: string) => boolean
|
||||||
/** When true (Hub), the unauthenticated profile button still opens the
|
|
||||||
* sheet so logged-out users can change theme/lang. When false (standalones),
|
|
||||||
* unauth profile button routes straight to /login. */
|
|
||||||
loggedOutOpensSheet?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), { loggedOutOpensSheet: false })
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
@ -73,10 +68,6 @@ function onTabClick(tab: BottomTab) {
|
||||||
{{ tab.badge > 99 ? '99+' : tab.badge }}
|
{{ tab.badge > 99 ? '99+' : tab.badge }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Always-on Profile entry, appended on the right. Consumers don't
|
|
||||||
pass it; the shell owns it so it's identical across every app. -->
|
|
||||||
<ProfileSheetTrigger :logged-out-opens-sheet="props.loggedOutOpensSheet" />
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { Home } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
/** Falls back to '/' for path-mount deployments where the hub root is the
|
|
||||||
* same origin. Set VITE_HUB_ROOT_URL to a full URL for subdomain
|
|
||||||
* deployments where the hub lives on a sibling origin. */
|
|
||||||
const hubRootUrl = computed(() => import.meta.env.VITE_HUB_ROOT_URL || '/')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<a
|
|
||||||
:href="hubRootUrl"
|
|
||||||
class="fixed top-0 right-0 z-40 m-3 inline-flex items-center gap-1.5 rounded-full border bg-background/90 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-3 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors shadow-sm"
|
|
||||||
style="margin-top: calc(env(safe-area-inset-top) + 0.75rem); margin-right: calc(env(safe-area-inset-right) + 0.75rem)"
|
|
||||||
:aria-label="t('common.nav.backToHub')"
|
|
||||||
>
|
|
||||||
<Home class="w-3.5 h-3.5" />
|
|
||||||
<span class="hidden sm:inline">{{ t('common.nav.hub') }}</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
@ -53,6 +53,9 @@ function goLogin() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- App-specific nav items (rendered by callers like StandaloneMenu) -->
|
||||||
|
<slot name="app-nav" />
|
||||||
|
|
||||||
<!-- Cross-app links + global preferences (always visible, auth or not) -->
|
<!-- Cross-app links + global preferences (always visible, auth or not) -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a
|
<a
|
||||||
|
|
|
||||||
81
src/components/layout/StandaloneMenu.vue
Normal file
81
src/components/layout/StandaloneMenu.vue
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, type Component } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Menu } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@/components/ui/sheet'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import ProfileSheetContent from './ProfileSheetContent.vue'
|
||||||
|
|
||||||
|
export interface SidebarNavItem {
|
||||||
|
/** Display label. */
|
||||||
|
name: string
|
||||||
|
/** Lucide (or any) component to render as the leading icon. */
|
||||||
|
icon: Component
|
||||||
|
/** Optional route to navigate to on click. */
|
||||||
|
path?: string
|
||||||
|
/** Optional click handler. Runs after navigation if both are set. */
|
||||||
|
onClick?: () => void
|
||||||
|
/** Visual-only "active" predicate for highlight state. */
|
||||||
|
isActive?: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** App-specific nav items rendered at the top of the sheet. */
|
||||||
|
items?: SidebarNavItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { items: () => [] })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const open = ref(false)
|
||||||
|
|
||||||
|
function handleClick(item: SidebarNavItem) {
|
||||||
|
if (item.path) router.push(item.path)
|
||||||
|
item.onClick?.()
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Sheet v-model:open="open">
|
||||||
|
<SheetTrigger as-child>
|
||||||
|
<button
|
||||||
|
class="fixed top-0 right-0 z-40 m-3 inline-flex items-center justify-center rounded-full border bg-background/90 backdrop-blur supports-[backdrop-filter]:bg-background/60 h-9 w-9 text-muted-foreground hover:text-foreground hover:bg-accent transition-colors shadow-sm"
|
||||||
|
style="margin-top: calc(env(safe-area-inset-top) + 0.75rem); margin-right: calc(env(safe-area-inset-right) + 0.75rem)"
|
||||||
|
:aria-label="t('common.nav.menu')"
|
||||||
|
>
|
||||||
|
<Menu class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="right" class="w-80 sm:w-96 overflow-y-auto">
|
||||||
|
<ProfileSheetContent>
|
||||||
|
<template v-if="props.items.length" #app-nav>
|
||||||
|
<nav class="mt-4 space-y-1">
|
||||||
|
<button
|
||||||
|
v-for="item in props.items"
|
||||||
|
:key="item.name"
|
||||||
|
type="button"
|
||||||
|
:class="[
|
||||||
|
item.isActive?.()
|
||||||
|
? 'bg-accent text-accent-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||||
|
'group flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||||
|
]"
|
||||||
|
@click="handleClick(item)"
|
||||||
|
>
|
||||||
|
<component :is="item.icon" class="h-5 w-5 shrink-0" />
|
||||||
|
{{ item.name }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<Separator class="mt-4" />
|
||||||
|
</template>
|
||||||
|
</ProfileSheetContent>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</template>
|
||||||
|
|
@ -3,9 +3,10 @@ import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { CalendarDays, Map, Heart, Search, Plus } from 'lucide-vue-next'
|
import { CalendarDays, Map, Heart, Search, Plus, Ticket, Megaphone } from 'lucide-vue-next'
|
||||||
import AppShell from '@/components/layout/AppShell.vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
|
import type { SidebarNavItem } from '@/components/layout/StandaloneMenu.vue'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { useEventsStore } from '@/modules/events/stores/events'
|
import { useEventsStore } from '@/modules/events/stores/events'
|
||||||
import { useEvents } from '@/modules/events/composables/useEvents'
|
import { useEvents } from '@/modules/events/composables/useEvents'
|
||||||
|
|
@ -24,7 +25,9 @@ const { isAdmin, autoApprove } = useApprovalState()
|
||||||
// Used to merge own LNbits drafts into the events feed right after
|
// Used to merge own LNbits drafts into the events feed right after
|
||||||
// the user creates or edits an event — otherwise the new draft only
|
// the user creates or edits an event — otherwise the new draft only
|
||||||
// surfaces on the next EventsPage subscribe cycle.
|
// surfaces on the next EventsPage subscribe cycle.
|
||||||
const { loadOwnEvents } = useEvents()
|
// The hosting filter also lives on the events composable; the
|
||||||
|
// sidebar entry below mirrors what the old in-page chip used to do.
|
||||||
|
const { loadOwnEvents, onlyHosting, toggleHosting } = useEvents()
|
||||||
|
|
||||||
// Settings dropped — theme/lang/currency now live in the shared profile sheet.
|
// Settings dropped — theme/lang/currency now live in the shared profile sheet.
|
||||||
// Create lives in the bottom nav: when logged out, tapping it shows an
|
// Create lives in the bottom nav: when logged out, tapping it shows an
|
||||||
|
|
@ -77,6 +80,31 @@ const tabs = computed<BottomTab[]>(() => [
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Sidebar entries shown to authed users only. "My tickets" routes to
|
||||||
|
// the dedicated /my-tickets page; "Hosting" toggles the feed filter
|
||||||
|
// (no dedicated page yet) and lands on /events so the user can
|
||||||
|
// see the filtered list.
|
||||||
|
const sidebarNav = computed<SidebarNavItem[]>(() => {
|
||||||
|
if (!isAuthenticated.value) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: t('events.filters.myTickets', 'My tickets'),
|
||||||
|
icon: Ticket,
|
||||||
|
path: '/my-tickets',
|
||||||
|
isActive: () => route.path.startsWith('/my-tickets'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('events.filters.hosting', 'Hosting'),
|
||||||
|
icon: Megaphone,
|
||||||
|
onClick: () => {
|
||||||
|
if (!onlyHosting.value) toggleHosting()
|
||||||
|
if (!route.path.startsWith('/events')) router.push('/events')
|
||||||
|
},
|
||||||
|
isActive: () => onlyHosting.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
// Feed tab is active for the bare /events route AND all sub-paths that
|
// Feed tab is active for the bare /events route AND all sub-paths that
|
||||||
// aren't owned by another tab (e.g. /events/<id> detail pages).
|
// aren't owned by another tab (e.g. /events/<id> detail pages).
|
||||||
function isActive(path: string): boolean {
|
function isActive(path: string): boolean {
|
||||||
|
|
@ -123,7 +151,7 @@ function handleDialogOpenChange(open: boolean) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppShell :tabs="tabs" :is-active="isActive">
|
<AppShell :tabs="tabs" :is-active="isActive" :sidebar-nav="sidebarNav">
|
||||||
<CreateEventDialog
|
<CreateEventDialog
|
||||||
:open="eventsStore.showCreateDialog"
|
:open="eventsStore.showCreateDialog"
|
||||||
:event="eventsStore.editingEvent"
|
:event="eventsStore.editingEvent"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const messages: LocaleMessages = {
|
||||||
profileDescription: 'Your Nostr identity and display name.',
|
profileDescription: 'Your Nostr identity and display name.',
|
||||||
profileLoggedOutDescription: 'Sign in or change your preferences.',
|
profileLoggedOutDescription: 'Sign in or change your preferences.',
|
||||||
login: 'Log in',
|
login: 'Log in',
|
||||||
|
menu: 'Menu',
|
||||||
backToHub: 'Back to hub',
|
backToHub: 'Back to hub',
|
||||||
hub: 'Hub',
|
hub: 'Hub',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const messages: LocaleMessages = {
|
||||||
profileDescription: 'Tu identidad Nostr y nombre de visualización.',
|
profileDescription: 'Tu identidad Nostr y nombre de visualización.',
|
||||||
profileLoggedOutDescription: 'Inicia sesión o cambia tus preferencias.',
|
profileLoggedOutDescription: 'Inicia sesión o cambia tus preferencias.',
|
||||||
login: 'Iniciar sesión',
|
login: 'Iniciar sesión',
|
||||||
|
menu: 'Menú',
|
||||||
backToHub: 'Volver al hub',
|
backToHub: 'Volver al hub',
|
||||||
hub: 'Hub',
|
hub: 'Hub',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const messages: LocaleMessages = {
|
||||||
profileDescription: 'Votre identité Nostr et votre nom d\u2019affichage.',
|
profileDescription: 'Votre identité Nostr et votre nom d\u2019affichage.',
|
||||||
profileLoggedOutDescription: 'Connectez-vous ou modifiez vos préférences.',
|
profileLoggedOutDescription: 'Connectez-vous ou modifiez vos préférences.',
|
||||||
login: 'Se connecter',
|
login: 'Se connecter',
|
||||||
|
menu: 'Menu',
|
||||||
backToHub: 'Retour au hub',
|
backToHub: 'Retour au hub',
|
||||||
hub: 'Hub',
|
hub: 'Hub',
|
||||||
theme: 'Thème',
|
theme: 'Thème',
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export interface LocaleMessages {
|
||||||
profileDescription: string
|
profileDescription: string
|
||||||
profileLoggedOutDescription: string
|
profileLoggedOutDescription: string
|
||||||
login: string
|
login: string
|
||||||
|
menu: string
|
||||||
backToHub: string
|
backToHub: string
|
||||||
hub: string
|
hub: string
|
||||||
theme: string
|
theme: string
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,8 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import { SlidersHorizontal, ChevronDown, Ticket, Megaphone, History } from 'lucide-vue-next'
|
import { SlidersHorizontal, ChevronDown, History } from 'lucide-vue-next'
|
||||||
import { useEvents } from '../composables/useEvents'
|
import { useEvents } from '../composables/useEvents'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
|
||||||
import EventSearchOverlay from '../components/EventSearchOverlay.vue'
|
import EventSearchOverlay from '../components/EventSearchOverlay.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'
|
||||||
|
|
@ -29,22 +28,16 @@ const {
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
onlyOwnedTickets,
|
|
||||||
onlyHosting,
|
|
||||||
showPast,
|
showPast,
|
||||||
selectDate,
|
selectDate,
|
||||||
setTemporal,
|
setTemporal,
|
||||||
toggleCategory,
|
toggleCategory,
|
||||||
clearCategories,
|
clearCategories,
|
||||||
toggleOwnedTickets,
|
|
||||||
toggleHosting,
|
|
||||||
togglePast,
|
togglePast,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
subscribe,
|
subscribe,
|
||||||
} = useEvents()
|
} = useEvents()
|
||||||
|
|
||||||
const { isAuthenticated } = useAuth()
|
|
||||||
|
|
||||||
const filtersOpen = ref(false)
|
const filtersOpen = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -83,32 +76,11 @@ function handleSelectEvent(event: Event) {
|
||||||
<TemporalFilterBar :model-value="temporal" @update:model-value="setTemporal" />
|
<TemporalFilterBar :model-value="temporal" @update:model-value="setTemporal" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Role + past-events filter chips. The role chips ("My tickets",
|
<!-- Past-events filter chip. The role chips ("My tickets", "Hosting")
|
||||||
"Hosting") narrow the feed to events the signed-in user
|
used to live here; they now sit in the standalone sidebar menu.
|
||||||
has skin in and are hidden when logged out. The "Past events"
|
"Past events" stays inline since past-browsing doesn't require
|
||||||
chip is always visible since past-browsing doesn't require an
|
an account and pairs visually with the temporal filters above. -->
|
||||||
account. -->
|
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
<div class="mb-4 flex flex-wrap gap-2">
|
||||||
<template v-if="isAuthenticated">
|
|
||||||
<Button
|
|
||||||
:variant="onlyOwnedTickets ? 'default' : 'outline'"
|
|
||||||
size="sm"
|
|
||||||
class="gap-1.5"
|
|
||||||
@click="toggleOwnedTickets"
|
|
||||||
>
|
|
||||||
<Ticket class="w-3.5 h-3.5" />
|
|
||||||
{{ t('events.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('events.filters.hosting', 'Hosting') }}
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<Button
|
<Button
|
||||||
:variant="showPast ? 'default' : 'outline'"
|
:variant="showPast ? 'default' : 'outline'"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue