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:
parent
ef042fed71
commit
7bc92e21b8
4 changed files with 44 additions and 45 deletions
|
|
@ -2,17 +2,34 @@
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
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 AppShell from '@/components/layout/AppShell.vue'
|
||||||
import type { BottomTab } from '@/components/layout/BottomNav.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 route = useRoute()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { isAuthenticated, currentUser } = useAuth()
|
||||||
|
const activitiesStore = useActivitiesStore()
|
||||||
|
|
||||||
// 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 (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[]>(() => [
|
const tabs = computed<BottomTab[]>(() => [
|
||||||
{ name: t('activities.nav.feed'), icon: Search, path: '/activities' },
|
{ name: t('activities.nav.feed'), icon: Search, path: '/activities' },
|
||||||
{ name: t('activities.nav.calendar'), icon: CalendarDays, path: '/activities/calendar' },
|
{ 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.map'), icon: Map, path: '/activities/map' },
|
||||||
{ name: t('activities.nav.favorites'), icon: Heart, path: '/activities/favorites' },
|
{ name: t('activities.nav.favorites'), icon: Heart, path: '/activities/favorites' },
|
||||||
])
|
])
|
||||||
|
|
@ -31,8 +48,23 @@ function isActive(path: string): boolean {
|
||||||
}
|
}
|
||||||
return route.path.startsWith(path)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -51,5 +51,9 @@ const isLoginPage = computed(() => route.path === '/login')
|
||||||
|
|
||||||
<HubPill v-if="!props.hideHub && !isLoginPage" />
|
<HubPill v-if="!props.hideHub && !isLoginPage" />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
||||||
|
<!-- Default slot for shell-level overlays (dialogs, sheets, etc.) that
|
||||||
|
need to live above route content but stay mounted across navigations. -->
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ export const useActivitiesStore = defineStore('activities', () => {
|
||||||
const activitiesMap = ref<Map<string, Activity>>(new Map())
|
const activitiesMap = ref<Map<string, Activity>>(new Map())
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const lastUpdated = ref<Date | null>(null)
|
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
|
// Computed
|
||||||
const activities = computed(() => Array.from(activitiesMap.value.values()))
|
const activities = computed(() => Array.from(activitiesMap.value.values()))
|
||||||
|
|
@ -84,6 +87,7 @@ export const useActivitiesStore = defineStore('activities', () => {
|
||||||
activitiesMap,
|
activitiesMap,
|
||||||
isLoading,
|
isLoading,
|
||||||
lastUpdated,
|
lastUpdated,
|
||||||
|
showCreateDialog,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
activities,
|
activities,
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,8 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import { SlidersHorizontal, ChevronDown, Plus } from 'lucide-vue-next'
|
import { SlidersHorizontal, ChevronDown } from 'lucide-vue-next'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
|
||||||
import { useActivities } from '../composables/useActivities'
|
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 ActivitySearchOverlay from '../components/ActivitySearchOverlay.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'
|
||||||
|
|
@ -24,9 +19,6 @@ import type { Activity } from '../types/activity'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { isAuthenticated } = useAuth()
|
|
||||||
|
|
||||||
const showCreateDialog = ref(false)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
activities,
|
activities,
|
||||||
|
|
@ -42,7 +34,6 @@ const {
|
||||||
clearCategories,
|
clearCategories,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
subscribe,
|
subscribe,
|
||||||
refresh,
|
|
||||||
} = useActivities()
|
} = useActivities()
|
||||||
|
|
||||||
const filtersOpen = ref(false)
|
const filtersOpen = ref(false)
|
||||||
|
|
@ -54,39 +45,15 @@ onMounted(() => {
|
||||||
function handleSelectActivity(activity: Activity) {
|
function handleSelectActivity(activity: Activity) {
|
||||||
router.push({ name: 'activity-detail', params: { id: activity.id } })
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto py-6 px-4">
|
<div class="container mx-auto py-6 px-4">
|
||||||
<!-- Page header -->
|
<!-- 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">
|
<h1 class="text-2xl sm:text-3xl font-bold text-foreground">
|
||||||
{{ t('activities.title') }}
|
{{ t('activities.title') }}
|
||||||
</h1>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Search with dropdown overlay -->
|
<!-- Search with dropdown overlay -->
|
||||||
|
|
@ -147,13 +114,5 @@ async function handleCreateEvent(eventData: CreateEventRequest) {
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
@select="handleSelectActivity"
|
@select="handleSelectActivity"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Create Event Dialog -->
|
|
||||||
<CreateEventDialog
|
|
||||||
:open="showCreateDialog"
|
|
||||||
@update:open="showCreateDialog = $event"
|
|
||||||
:on-create-event="handleCreateEvent"
|
|
||||||
@event-created="handleRefresh"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue