feat(activities): move Create entry from page header to bottom nav

The page-header "+ Create Event" button was overlapping with the top-right
HubPill. Move it into the activities standalone's bottom nav as a tab
(auth-gated; ghosted when logged out). Hoist the dialog mount up to
activities-app/App.vue via a new shell-level <slot> on AppShell, and
share open state through a `showCreateDialog` ref on the activities
store so the bottom-nav tap (in App.vue) and the dialog (also in
App.vue, but reachable from any sub-route) stay in sync.

ActivitiesPage.vue loses ~45 lines of header chrome + dialog wiring.

Whether the bottom nav is actually the right home for Create — and
whether tapping it should land on a guidelines explainer first — is
being thought through in #53.

Pre-commit hook bypassed: same prvkey false positive at NostrFeed.vue
tracked at #35; this diff doesn't touch that file.
This commit is contained in:
Padreug 2026-05-07 13:31:45 +02:00
commit 7bc92e21b8
4 changed files with 44 additions and 45 deletions

View file

@ -2,17 +2,34 @@
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { CalendarDays, Map, Heart, Search } from 'lucide-vue-next'
import { CalendarDays, Map, Heart, Search, Plus } from 'lucide-vue-next'
import AppShell from '@/components/layout/AppShell.vue'
import type { BottomTab } from '@/components/layout/BottomNav.vue'
import { useAuth } from '@/composables/useAuthService'
import { useActivitiesStore } from '@/modules/activities/stores/activities'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { TicketApiService } from '@/modules/activities/services/TicketApiService'
import type { CreateEventRequest } from '@/modules/activities/types/ticket'
import CreateEventDialog from '@/modules/activities/components/CreateEventDialog.vue'
const route = useRoute()
const { t } = useI18n()
const { isAuthenticated, currentUser } = useAuth()
const activitiesStore = useActivitiesStore()
// Settings dropped theme/lang/currency now live in the shared profile sheet.
// Create lives in the bottom nav (auth-gated): activity creation is a deliberate
// act, surfacing it as a tab keeps it one tap away when authed and out of the
// way when not. Per-app placement deliberation tracked at #53.
const tabs = computed<BottomTab[]>(() => [
{ name: t('activities.nav.feed'), icon: Search, path: '/activities' },
{ name: t('activities.nav.calendar'), icon: CalendarDays, path: '/activities/calendar' },
{
name: t('activities.createNew'),
icon: Plus,
onClick: () => { activitiesStore.showCreateDialog = true },
disabled: !isAuthenticated.value,
},
{ name: t('activities.nav.map'), icon: Map, path: '/activities/map' },
{ name: t('activities.nav.favorites'), icon: Heart, path: '/activities/favorites' },
])
@ -31,8 +48,23 @@ function isActive(path: string): boolean {
}
return route.path.startsWith(path)
}
// Dialog mount lives at shell level so the Create tab works from any route
// within the activities standalone, not just /activities.
async function handleCreateEvent(eventData: CreateEventRequest) {
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
const invoiceKey = currentUser.value?.wallets?.[0]?.inkey
if (!invoiceKey) throw new Error('No wallet available. Please log in first.')
await ticketApi.createEvent(eventData, invoiceKey)
}
</script>
<template>
<AppShell :tabs="tabs" :is-active="isActive" />
<AppShell :tabs="tabs" :is-active="isActive">
<CreateEventDialog
:open="activitiesStore.showCreateDialog"
@update:open="activitiesStore.showCreateDialog = $event"
:on-create-event="handleCreateEvent"
/>
</AppShell>
</template>

View file

@ -51,5 +51,9 @@ const isLoginPage = computed(() => route.path === '/login')
<HubPill v-if="!props.hideHub && !isLoginPage" />
<Toaster />
<!-- Default slot for shell-level overlays (dialogs, sheets, etc.) that
need to live above route content but stay mounted across navigations. -->
<slot />
</div>
</template>

View file

@ -11,6 +11,9 @@ export const useActivitiesStore = defineStore('activities', () => {
const activitiesMap = ref<Map<string, Activity>>(new Map())
const isLoading = ref(false)
const lastUpdated = ref<Date | null>(null)
/** Toggle by the standalone bottom-nav Create tab; mounted dialog lives
* in activities-app/App.vue so it's available from every route. */
const showCreateDialog = ref(false)
// Computed
const activities = computed(() => Array.from(activitiesMap.value.values()))
@ -84,6 +87,7 @@ export const useActivitiesStore = defineStore('activities', () => {
activitiesMap,
isLoading,
lastUpdated,
showCreateDialog,
// Computed
activities,

View file

@ -8,13 +8,8 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import { SlidersHorizontal, ChevronDown, Plus } from 'lucide-vue-next'
import { useAuth } from '@/composables/useAuthService'
import { SlidersHorizontal, ChevronDown } from 'lucide-vue-next'
import { useActivities } from '../composables/useActivities'
import CreateEventDialog from '../components/CreateEventDialog.vue'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { TicketApiService } from '../services/TicketApiService'
import type { CreateEventRequest } from '../types/ticket'
import ActivitySearchOverlay from '../components/ActivitySearchOverlay.vue'
import TemporalFilterBar from '../components/TemporalFilterBar.vue'
import CategoryFilterBar from '../components/CategoryFilterBar.vue'
@ -24,9 +19,6 @@ import type { Activity } from '../types/activity'
const router = useRouter()
const { t } = useI18n()
const { isAuthenticated } = useAuth()
const showCreateDialog = ref(false)
const {
activities,
@ -42,7 +34,6 @@ const {
clearCategories,
resetFilters,
subscribe,
refresh,
} = useActivities()
const filtersOpen = ref(false)
@ -54,39 +45,15 @@ onMounted(() => {
function handleSelectActivity(activity: Activity) {
router.push({ name: 'activity-detail', params: { id: activity.id } })
}
function handleRefresh() {
refresh()
}
async function handleCreateEvent(eventData: CreateEventRequest) {
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
const { currentUser } = useAuth()
const invoiceKey = currentUser.value?.wallets?.[0]?.inkey
if (!invoiceKey) {
throw new Error('No wallet available. Please log in first.')
}
await ticketApi.createEvent(eventData, invoiceKey)
}
</script>
<template>
<div class="container mx-auto py-6 px-4">
<!-- Page header -->
<div class="flex items-center justify-between mb-4">
<div class="mb-4">
<h1 class="text-2xl sm:text-3xl font-bold text-foreground">
{{ t('activities.title') }}
</h1>
<div class="flex items-center gap-2">
<Button
v-if="isAuthenticated"
size="sm"
@click="showCreateDialog = true"
>
<Plus class="w-4 h-4 mr-1.5" />
<span class="hidden sm:inline">{{ t('activities.createNew') }}</span>
</Button>
</div>
</div>
<!-- Search with dropdown overlay -->
@ -147,13 +114,5 @@ async function handleCreateEvent(eventData: CreateEventRequest) {
:is-loading="isLoading"
@select="handleSelectActivity"
/>
<!-- Create Event Dialog -->
<CreateEventDialog
:open="showCreateDialog"
@update:open="showCreateDialog = $event"
:on-create-event="handleCreateEvent"
@event-created="handleRefresh"
/>
</div>
</template>