From a48e3ace5f0f3eba0982233a966bdfd11d519494 Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 4 Jun 2026 22:21:41 +0200 Subject: [PATCH] feat(activities): restructure bottom nav around Home/MyTickets/Hosting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bottom-nav tabs become Home, My tickets, Hosting, Map, Favorites. - Feed is relabeled "Home" (en/fr; es was already "Inicio"). - My tickets and Hosting move out of the sidebar menu back into the bottom nav. Hosting is a synthetic tab — no path of its own; it toggles the existing onlyHosting feed filter and lands on /activities, with Home as the inverse (clears the filter on tap). - Calendar leaves the bottom nav. The week strip now ends with a small calendar icon button that routes to /activities/calendar, so the entry point sits adjacent to the date UI instead of competing for a tab slot. - Create activity leaves the bottom nav too. A full-width "+ Create activity" CTA appears at the top of the feed only when the Hosting tab is active, so the Create entry point lives inside the section it belongs to. BottomTab gains an optional `isActive()` predicate so tabs whose active condition doesn't reduce to "current path starts with x" (e.g. Hosting) can compute their own state. --- src/activities-app/App.vue | 109 +++++++++--------- src/components/layout/BottomNav.vue | 14 ++- src/i18n/locales/en.ts | 2 +- src/i18n/locales/fr.ts | 2 +- .../activities/views/ActivitiesPage.vue | 52 ++++++++- 5 files changed, 115 insertions(+), 64 deletions(-) diff --git a/src/activities-app/App.vue b/src/activities-app/App.vue index 829ba78..663fb38 100644 --- a/src/activities-app/App.vue +++ b/src/activities-app/App.vue @@ -3,10 +3,9 @@ import { computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' import { toast } from 'vue-sonner' -import { CalendarDays, Map, Heart, Search, Plus, Ticket, Megaphone } from 'lucide-vue-next' +import { Home, Map, Heart, Ticket, Megaphone } from 'lucide-vue-next' import AppShell from '@/components/layout/AppShell.vue' import type { BottomTab } from '@/components/layout/BottomNav.vue' -import type { SidebarNavItem } from '@/components/layout/StandaloneMenu.vue' import { useAuth } from '@/composables/useAuthService' import { useActivitiesStore } from '@/modules/activities/stores/activities' import { useActivities } from '@/modules/activities/composables/useActivities' @@ -24,38 +23,71 @@ const activitiesStore = useActivitiesStore() const { isAdmin, autoApprove } = useApprovalState() // Used to merge own LNbits drafts into the activities feed right after // the user creates or edits an event — otherwise the new draft only -// surfaces on the next ActivitiesPage subscribe cycle. -// The hosting filter also lives on the activities composable; the -// sidebar entry below mirrors what the old in-page chip used to do. +// surfaces on the next ActivitiesPage subscribe cycle. `onlyHosting` +// is the feed filter that backs the Hosting bottom-nav tab — tapping +// it toggles the filter on; Home tab toggles it off. const { loadOwnEvents, onlyHosting, toggleHosting } = useActivities() -// 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 -// auth-prompt toast (mirroring BookmarkButton/RSVPButton) instead of -// opening the dialog. Per-app placement deliberation tracked at #53. +// True for /activities and its sub-routes (incl. detail pages) but +// not for the routes owned by other tabs (map/favorites). Used by +// both Home and Hosting active-state predicates so the highlight +// only shifts based on the onlyHosting flag while you're in the feed. +function inFeedRoute(): boolean { + if (route.path.startsWith('/activities/map')) return false + if (route.path.startsWith('/activities/favorites')) return false + return route.path === '/activities' || route.path.startsWith('/activities/') +} + const tabs = computed(() => [ - { 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, + name: t('activities.nav.feed'), + icon: Home, + onClick: () => { + // Tapping Home clears the hosting filter so the feed always + // returns to the unfiltered view, regardless of where the + // user just came from. + if (onlyHosting.value) toggleHosting() + if (route.path !== '/activities') router.push('/activities') + }, + isActive: () => inFeedRoute() && !onlyHosting.value, + }, + { + name: t('activities.filters.myTickets'), + icon: Ticket, + path: '/my-tickets', onClick: () => { if (!isAuthenticated.value) { - toast.info('Log in to create an activity', { + toast.info(t('activities.detail.loginToBuyTickets'), { action: { - label: 'Log in', + label: t('activities.detail.logIn'), onClick: () => router.push('/login'), }, }) return } - // Defensively clear any lingering edit selection so the Create - // tap always opens in Create mode regardless of a prior Edit. - activitiesStore.editingEvent = null - activitiesStore.showCreateDialog = true + router.push('/my-tickets') }, disabled: !isAuthenticated.value, }, + { + name: t('activities.filters.hosting'), + icon: Megaphone, + onClick: () => { + if (!isAuthenticated.value) { + toast.info(t('activities.hosting.loginPrompt', 'Log in to manage your hosted activities'), { + action: { + label: t('activities.favorites.logIn'), + onClick: () => router.push('/login'), + }, + }) + return + } + if (!onlyHosting.value) toggleHosting() + if (route.path !== '/activities') router.push('/activities') + }, + isActive: () => inFeedRoute() && onlyHosting.value, + disabled: !isAuthenticated.value, + }, { name: t('activities.nav.map'), icon: Map, path: '/activities/map' }, { name: t('activities.nav.favorites'), @@ -80,43 +112,8 @@ const tabs = computed(() => [ }, ]) -// 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 /activities so the user can -// see the filtered list. -const sidebarNav = computed(() => { - if (!isAuthenticated.value) return [] - return [ - { - name: t('activities.filters.myTickets', 'My tickets'), - icon: Ticket, - path: '/my-tickets', - isActive: () => route.path.startsWith('/my-tickets'), - }, - { - name: t('activities.filters.hosting', 'Hosting'), - icon: Megaphone, - onClick: () => { - if (!onlyHosting.value) toggleHosting() - if (!route.path.startsWith('/activities')) router.push('/activities') - }, - isActive: () => onlyHosting.value, - }, - ] -}) - -// Feed tab is active for the bare /activities route AND all sub-paths that -// aren't owned by another tab (e.g. /activities/ detail pages). +// Path-based fallback for tabs that don't carry their own `isActive`. function isActive(path: string): boolean { - if (path === '/activities') { - return ( - route.path === '/activities' || - (route.path.startsWith('/activities/') && - !route.path.startsWith('/activities/calendar') && - !route.path.startsWith('/activities/map') && - !route.path.startsWith('/activities/favorites')) - ) - } return route.path.startsWith(path) } @@ -151,7 +148,7 @@ function handleDialogOpenChange(open: boolean) {